[permissions] Filter tabs + registered actions according to permissions (#12657)

Note and task tabs in side panel should only show if user has reading
permission on them.

"Go to companies", "Go to workflows", etc. in command menu should only
show is user has reading permission on related objects.

<img width="507" alt="Capture d’écran 2025-06-17 à 11 09 50"
src="https://github.com/user-attachments/assets/3a2a4c25-0b9b-4ee6-b18f-b019b8a56d47"
/>
<img width="505" alt="Capture d’écran 2025-06-17 à 11 09 56"
src="https://github.com/user-attachments/assets/8a219955-cc8e-4dbf-a4f9-a50e1aaa4b59"
/>

**How to test** 
Assign a user with a custom role that has **no** read permissions on
notes/tasks/workflows/companies/opportunities/people (no need to test
them all but at least one between note and tasks; workflows; one between
companies/opportunities/people). Check that you don't see the related
tab / action.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-06-18 17:12:58 +02:00
committed by GitHub
parent e77e7e3149
commit da5ae34109
12 changed files with 206 additions and 35 deletions

View File

@ -410,7 +410,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconSettingsAutomation,
accent: 'default',
isPinned: false,
shouldBeRegistered: ({ objectMetadataItem, viewType, isWorkflowEnabled }) =>
shouldBeRegistered: ({
objectMetadataItem,
viewType,
isWorkflowEnabled,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Workflow) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Workflow ||
viewType === ActionViewType.SHOW_PAGE) &&
isWorkflowEnabled,
@ -443,9 +449,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Person ||
viewType === ActionViewType.SHOW_PAGE,
shouldBeRegistered: ({
objectMetadataItem,
viewType,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Person) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Person ||
viewType === ActionViewType.SHOW_PAGE),
component: (
<ActionLink
to={AppPath.RecordIndexPage}
@ -469,9 +480,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Company ||
viewType === ActionViewType.SHOW_PAGE,
shouldBeRegistered: ({
objectMetadataItem,
viewType,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Company) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Company ||
viewType === ActionViewType.SHOW_PAGE),
component: (
<ActionLink
to={AppPath.RecordIndexPage}
@ -495,9 +511,16 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Opportunity ||
viewType === ActionViewType.SHOW_PAGE,
shouldBeRegistered: ({
objectMetadataItem,
viewType,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Opportunity) ===
true &&
(objectMetadataItem?.nameSingular !==
CoreObjectNameSingular.Opportunity ||
viewType === ActionViewType.SHOW_PAGE),
component: (
<ActionLink
to={AppPath.RecordIndexPage}
@ -547,9 +570,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Task ||
viewType === ActionViewType.SHOW_PAGE,
shouldBeRegistered: ({
objectMetadataItem,
viewType,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Task) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Task ||
viewType === ActionViewType.SHOW_PAGE),
component: (
<ActionLink
to={AppPath.RecordIndexPage}
@ -573,9 +601,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Note ||
viewType === ActionViewType.SHOW_PAGE,
shouldBeRegistered: ({
objectMetadataItem,
viewType,
getTargetObjectReadPermission,
}) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Note) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Note ||
viewType === ActionViewType.SHOW_PAGE),
component: (
<ActionLink
to={AppPath.RecordIndexPage}

View File

@ -20,4 +20,7 @@ export type ShouldBeRegisteredFunctionParams = {
numberOfSelectedRecords?: number;
workflowWithCurrentVersion?: WorkflowWithCurrentVersion;
viewType?: ActionViewType;
getTargetObjectReadPermission: (
objectMetadataItemNameSingular: string,
) => boolean;
};

View File

@ -7,20 +7,25 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
export const getActionConfig = (
objectMetadataItem?: ObjectMetadataItem,
): Record<string, ActionConfig> => {
export const getActionConfig = ({
objectMetadataItem,
}: {
objectMetadataItem?: ObjectMetadataItem;
}): Record<string, ActionConfig> => {
if (!isDefined(objectMetadataItem)) {
return {};
}
switch (objectMetadataItem.nameSingular) {
case CoreObjectNameSingular.Workflow:
case CoreObjectNameSingular.Workflow: {
return WORKFLOW_ACTIONS_CONFIG;
case CoreObjectNameSingular.WorkflowVersion:
}
case CoreObjectNameSingular.WorkflowVersion: {
return WORKFLOW_VERSIONS_ACTIONS_CONFIG;
case CoreObjectNameSingular.WorkflowRun:
}
case CoreObjectNameSingular.WorkflowRun: {
return WORKFLOW_RUNS_ACTIONS_CONFIG;
}
default:
return DEFAULT_RECORD_ACTIONS_CONFIG;
}

View File

@ -26,7 +26,9 @@ export const useRegisteredActions = (
contextStoreTargetedRecordsRule,
);
const recordActionConfig = getActionConfig(objectMetadataItem);
const recordActionConfig = getActionConfig({
objectMetadataItem,
});
const recordAgnosticActionConfig = RECORD_AGNOSTIC_ACTIONS_CONFIG;

View File

@ -1,6 +1,7 @@
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -14,7 +15,7 @@ import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-t
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useShouldActionBeRegisteredParams = ({
@ -77,6 +78,20 @@ export const useShouldActionBeRegisteredParams = ({
contextStoreTargetedRecordsRule,
);
const getObjectReadPermission = useRecoilCallback(
({ snapshot }) =>
(objectMetadataNameSingular: string) => {
return snapshot
.getLoadable(
objectPermissionsFamilySelector({
objectNameSingular: objectMetadataNameSingular,
}),
)
.getValue().canRead;
},
[],
);
return {
objectMetadataItem,
isFavorite,
@ -89,5 +104,6 @@ export const useShouldActionBeRegisteredParams = ({
isWorkflowEnabled,
numberOfSelectedRecords,
viewType: viewType ?? undefined,
getTargetObjectReadPermission: getObjectReadPermission,
};
};