Field permission CRUD front-end implementation (#13524)

This PR implements what's needed to edit field permissions on a role.

Field permissions that aren't useful are kept in the database to avoid
the overhead of adding cleaning logics both in front end and back end.
They just won't be taken into account if object permission doesn't allow
it.

In this PR we also handle the case were there's no object permission
override but where there are only field permission overrides, which can
happen if an object inherits from a "all object can read" but restricts
read only on some fields.

<img width="547" height="642" alt="image"
src="https://github.com/user-attachments/assets/77d81f89-4af9-42b6-97f3-fae3a6ba1eeb"
/>


<img width="590" height="912" alt="image"
src="https://github.com/user-attachments/assets/69fab8ee-7252-401a-bc6f-8a8b7c7f6bc4"
/>

Fixes https://github.com/twentyhq/core-team-issues/issues/1152

This PR lowers unit test coverage because the essential unit tests for
this PR have been written and writing more tests would not be a great
tradeoff for this feature which has already taken a lot of efforts.
This commit is contained in:
Lucas Bordeau
2025-08-05 11:50:23 +02:00
committed by GitHub
parent 497fc2c0b2
commit 04ed843aaf
59 changed files with 2358 additions and 527 deletions
+2 -2
View File
@@ -52,9 +52,9 @@ const jestConfig: JestConfigWithTsJest = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 56.7,
statements: 56,
lines: 55,
functions: 46,
functions: 45,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
@@ -2455,6 +2455,7 @@ export type Role = {
canUpdateAllObjectRecords: Scalars['Boolean'];
canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
fieldPermissions?: Maybe<Array<FieldPermission>>;
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isEditable: Scalars['Boolean'];
@@ -3940,6 +3941,8 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean } };
export type FieldPermissionFragmentFragment = { __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string };
export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null };
export type PermissionFlagFragmentFragment = { __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string };
@@ -3975,6 +3978,13 @@ export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{
export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } };
export type UpsertFieldPermissionsMutationVariables = Exact<{
upsertFieldPermissionsInput: UpsertFieldPermissionsInput;
}>;
export type UpsertFieldPermissionsMutation = { __typename?: 'Mutation', upsertFieldPermissions: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> };
export type UpsertObjectPermissionsMutationVariables = Exact<{
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
}>;
@@ -3992,7 +4002,7 @@ export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPer
export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null }> };
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null }> };
export type CreateApprovedAccessDomainMutationVariables = Exact<{
input: CreateApprovedAccessDomainInput;
@@ -4393,6 +4403,16 @@ export const WebhookFragmentFragmentDoc = gql`
secret
}
`;
export const FieldPermissionFragmentFragmentDoc = gql`
fragment FieldPermissionFragment on FieldPermission {
objectMetadataId
fieldMetadataId
canReadFieldValue
canUpdateFieldValue
id
roleId
}
`;
export const PermissionFlagFragmentFragmentDoc = gql`
fragment PermissionFlagFragment on PermissionFlag {
id
@@ -8358,6 +8378,41 @@ export function useUpdateWorkspaceMemberRoleMutation(baseOptions?: Apollo.Mutati
export type UpdateWorkspaceMemberRoleMutationHookResult = ReturnType<typeof useUpdateWorkspaceMemberRoleMutation>;
export type UpdateWorkspaceMemberRoleMutationResult = Apollo.MutationResult<UpdateWorkspaceMemberRoleMutation>;
export type UpdateWorkspaceMemberRoleMutationOptions = Apollo.BaseMutationOptions<UpdateWorkspaceMemberRoleMutation, UpdateWorkspaceMemberRoleMutationVariables>;
export const UpsertFieldPermissionsDocument = gql`
mutation UpsertFieldPermissions($upsertFieldPermissionsInput: UpsertFieldPermissionsInput!) {
upsertFieldPermissions(
upsertFieldPermissionsInput: $upsertFieldPermissionsInput
) {
...FieldPermissionFragment
}
}
${FieldPermissionFragmentFragmentDoc}`;
export type UpsertFieldPermissionsMutationFn = Apollo.MutationFunction<UpsertFieldPermissionsMutation, UpsertFieldPermissionsMutationVariables>;
/**
* __useUpsertFieldPermissionsMutation__
*
* To run a mutation, you first call `useUpsertFieldPermissionsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpsertFieldPermissionsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [upsertFieldPermissionsMutation, { data, loading, error }] = useUpsertFieldPermissionsMutation({
* variables: {
* upsertFieldPermissionsInput: // value for 'upsertFieldPermissionsInput'
* },
* });
*/
export function useUpsertFieldPermissionsMutation(baseOptions?: Apollo.MutationHookOptions<UpsertFieldPermissionsMutation, UpsertFieldPermissionsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpsertFieldPermissionsMutation, UpsertFieldPermissionsMutationVariables>(UpsertFieldPermissionsDocument, options);
}
export type UpsertFieldPermissionsMutationHookResult = ReturnType<typeof useUpsertFieldPermissionsMutation>;
export type UpsertFieldPermissionsMutationResult = Apollo.MutationResult<UpsertFieldPermissionsMutation>;
export type UpsertFieldPermissionsMutationOptions = Apollo.BaseMutationOptions<UpsertFieldPermissionsMutation, UpsertFieldPermissionsMutationVariables>;
export const UpsertObjectPermissionsDocument = gql`
mutation UpsertObjectPermissions($upsertObjectPermissionsInput: UpsertObjectPermissionsInput!) {
upsertObjectPermissions(
@@ -8439,12 +8494,16 @@ export const GetRolesDocument = gql`
objectPermissions {
...ObjectPermissionFragment
}
fieldPermissions {
...FieldPermissionFragment
}
}
}
${RoleFragmentFragmentDoc}
${WorkspaceMemberQueryFragmentFragmentDoc}
${PermissionFlagFragmentFragmentDoc}
${ObjectPermissionFragmentFragmentDoc}`;
${ObjectPermissionFragmentFragmentDoc}
${FieldPermissionFragmentFragmentDoc}`;
/**
* __useGetRolesQuery__
@@ -2301,6 +2301,7 @@ export type Role = {
canUpdateAllObjectRecords: Scalars['Boolean'];
canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
fieldPermissions?: Maybe<Array<FieldPermission>>;
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isEditable: Scalars['Boolean'];
@@ -0,0 +1,7 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export const filterUserFacingFieldMetadataItems = (
field: FieldMetadataItem,
) => {
return !field.isSystem;
};
@@ -2,9 +2,9 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
import { IconComponent, IconTwentyStar } from 'twenty-ui/display';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
type SettingsObjectFieldDataTypeProps = {
to?: string;
@@ -10,6 +10,7 @@ import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/o
import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown';
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
import { FIELD_LEVEL_PERMISSION_TABLE_GRID_TEMPLATE_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/constants/FieldLevelPermissionTableGridTemplateColumns';
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
@@ -39,7 +40,7 @@ type SettingsObjectFieldItemTableRowProps = {
};
export const StyledObjectFieldTableRow = styled(TableRow)`
grid-template-columns: 180px 148px 148px 36px;
grid-template-columns: ${FIELD_LEVEL_PERMISSION_TABLE_GRID_TEMPLATE_COLUMNS};
`;
const StyledNameTableCell = styled(TableCell)`
@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const FIELD_PERMISSION_FRAGMENT = gql`
fragment FieldPermissionFragment on FieldPermission {
objectMetadataId
fieldMetadataId
canReadFieldValue
canUpdateFieldValue
id
roleId
}
`;
@@ -0,0 +1,15 @@
import { FIELD_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/fieldPermissionFragment';
import { gql } from '@apollo/client';
export const UPSERT_FIELD_PERMISSIONS = gql`
${FIELD_PERMISSION_FRAGMENT}
mutation UpsertFieldPermissions(
$upsertFieldPermissionsInput: UpsertFieldPermissionsInput!
) {
upsertFieldPermissions(
upsertFieldPermissionsInput: $upsertFieldPermissionsInput
) {
...FieldPermissionFragment
}
}
`;
@@ -1,3 +1,4 @@
import { FIELD_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/fieldPermissionFragment';
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
@@ -9,6 +10,7 @@ export const GET_ROLES = gql`
${ROLE_FRAGMENT}
${PERMISSION_FLAG_FRAGMENT}
${OBJECT_PERMISSION_FRAGMENT}
${FIELD_PERMISSION_FRAGMENT}
query GetRoles {
getRoles {
...RoleFragment
@@ -21,6 +23,9 @@ export const GET_ROLES = gql`
objectPermissions {
...ObjectPermissionFragment
}
fieldPermissions {
...FieldPermissionFragment
}
}
}
`;
@@ -1,15 +1,13 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { SettingsCard } from '@/settings/components/SettingsCard';
import { hasPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/utils/hasPermissionOverride';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useFilterObjectMetadataItemsWithPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride';
import { useObjectMetadataItemsThatCanHavePermission } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useObjectMetadataItemsThatCanHavePermission';
import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useMemo, useState } from 'react';
import { useRecoilState } from 'recoil';
import { useState } from 'react';
import { H2Title, IconSearch, useIcons } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@@ -55,12 +53,9 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
const theme = useTheme();
const navigate = useNavigateSettings();
const [searchFilter, setSearchFilter] = useState('');
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const { objectMetadataItemsThatCanHavePermission } =
useObjectMetadataItemsThatCanHavePermission();
const { getIcon } = useIcons();
@@ -69,49 +64,30 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
};
const handleSelectObjectMetadata = (objectMetadataId: string) => {
setSettingsDraftRole((draftRole) => ({
...draftRole,
objectPermissions: [
...(draftRole.objectPermissions ?? []).filter(
(permission) => permission.objectMetadataId !== objectMetadataId,
),
{
objectMetadataId,
canReadObjectRecords: null,
canUpdateObjectRecords: null,
canSoftDeleteObjectRecords: null,
canDestroyObjectRecords: null,
},
],
}));
navigate(SettingsPath.RoleObjectLevel, {
roleId,
objectMetadataId,
});
};
const excludedObjectMetadataIds = useMemo(
() =>
settingsDraftRole.objectPermissions
?.filter((objectPermission) =>
hasPermissionOverride(objectPermission, settingsDraftRole),
)
.map((p) => p.objectMetadataId) ?? [],
[settingsDraftRole],
);
const { filterObjectMetadataItemsWithPermissionOverride } =
useFilterObjectMetadataItemsWithPermissionOverride({
roleId,
});
const filteredObjectMetadataItems = useMemo(
() =>
objectMetadataItems.filter(
(objectMetadataItem) =>
objectMetadataItem.labelPlural
.toLowerCase()
.includes(searchFilter.toLowerCase()) &&
!excludedObjectMetadataIds.includes(objectMetadataItem.id) &&
!isWorkflowRelatedObjectMetadata(objectMetadataItem.nameSingular),
),
[objectMetadataItems, searchFilter, excludedObjectMetadataIds],
);
const objectMetadataItemIdsWithPermission =
objectMetadataItemsThatCanHavePermission
.filter(filterObjectMetadataItemsWithPermissionOverride)
.map((objectMetadataItem) => objectMetadataItem.id);
const filteredObjectMetadataItems =
objectMetadataItemsThatCanHavePermission.filter(
(objectMetadataItem) =>
objectMetadataItem.labelPlural
.toLowerCase()
.includes(searchFilter.toLowerCase()) &&
!objectMetadataItemIdsWithPermission.includes(objectMetadataItem.id),
);
const standardObjects = filteredObjectMetadataItems.filter(
(item) => !item.isCustom,
@@ -167,7 +143,6 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
</StyledContainer>
</Section>
)}
{customObjects.length > 0 && (
<Section>
<H2Title title={t`Custom`} description={t`All your custom objects`} />
@@ -1,3 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { objectPermissionKeyToHumanReadable } from '@/settings/roles/role-permissions/object-level-permissions/utils/objectPermissionKeyToHumanReadableText';
import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
@@ -8,21 +9,20 @@ import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { AppTooltip, TooltipDelay } from 'twenty-ui/display';
import { ObjectPermission } from '~/generated/graphql';
const StyledContainer = styled.div`
display: flex;
`;
type SettingsRolePermissionsObjectLevelOverrideCellProps = {
objectPermissions: ObjectPermission;
objectMetadataItem: ObjectMetadataItem;
objectPermissionKey: SettingsRoleObjectPermissionKey;
roleId: string;
objectLabel: string;
};
export const SettingsRolePermissionsObjectLevelOverrideCell = ({
objectPermissions,
objectMetadataItem,
objectPermissionKey,
roleId,
objectLabel,
@@ -36,7 +36,12 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
const permissionValue = objectPermissions[objectPermissionKey];
const objectPermission = settingsDraftRole.objectPermissions?.find(
(objectPermission) =>
objectPermission.objectMetadataId === objectMetadataItem.id,
);
const permissionValue = objectPermission?.[objectPermissionKey];
const isOverridden = (
objectPermissionKey: SettingsRoleObjectPermissionKey,
@@ -56,7 +61,7 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
const humanReadableAction =
objectPermissionKeyToHumanReadable(objectPermissionKey);
const containerId = `object-level-permission-override-${roleId}-${objectPermissionKey}`;
const containerId = `object-level-permission-override-${roleId}-${objectPermissionKey}-${objectMetadataItem.id}`;
return (
<>
@@ -1,7 +1,7 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import styled from '@emotion/styled';
import { ObjectPermission } from '~/generated/graphql';
import { SettingsRolePermissionsObjectLevelOverrideCell } from './SettingsRolePermissionsObjectLevelOverrideCell';
const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div`
@@ -10,13 +10,13 @@ const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div`
`;
type SettingsRolePermissionsObjectLevelOverrideCellContainerProps = {
objectPermissions: ObjectPermission;
objectMetadataItem: ObjectMetadataItem;
roleId: string;
objectLabel: string;
};
export const SettingsRolePermissionsObjectLevelOverrideCellContainer = ({
objectPermissions,
objectMetadataItem,
roleId,
objectLabel,
}: SettingsRolePermissionsObjectLevelOverrideCellContainerProps) => {
@@ -31,7 +31,7 @@ export const SettingsRolePermissionsObjectLevelOverrideCellContainer = ({
return (
<SettingsRolePermissionsObjectLevelOverrideCell
key={permissionKey}
objectPermissions={objectPermissions}
objectMetadataItem={objectMetadataItem}
objectPermissionKey={permissionKey}
roleId={roleId}
objectLabel={objectLabel}
@@ -1,16 +1,12 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
import { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader';
import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow';
import { hasPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/utils/hasPermissionOverride';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useFilterObjectMetadataItemsWithPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride';
import { useObjectMetadataItemsThatCanHavePermission } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useObjectMetadataItemsThatCanHavePermission';
import { SettingsPath } from '@/types/SettingsPath';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -43,37 +39,24 @@ export const SettingsRolePermissionsObjectLevelSection = ({
roleId,
isEditable,
}: SettingsRolePermissionsObjectLevelSectionProps) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const navigateSettings = useNavigateSettings();
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const { objectMetadataItemsThatCanHavePermission } =
useObjectMetadataItemsThatCanHavePermission();
const filteredObjectMetadataItems = objectMetadataItems.filter(
(item) => !isWorkflowRelatedObjectMetadata(item.nameSingular),
);
const { filterObjectMetadataItemsWithPermissionOverride } =
useFilterObjectMetadataItemsWithPermissionOverride({
roleId,
});
const objectMetadataMap = filteredObjectMetadataItems.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, ObjectMetadataItem>,
);
const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter(
(objectPermission) =>
hasPermissionOverride(objectPermission, settingsDraftRole) &&
!isWorkflowRelatedObjectMetadata(
objectMetadataMap[objectPermission.objectMetadataId]?.nameSingular,
),
);
const objectMetadataItemsWithPermissionOverride =
objectMetadataItemsThatCanHavePermission.filter(
filterObjectMetadataItemsWithPermissionOverride,
);
const allObjectsHaveSetPermission =
filteredObjectPermissions?.length === filteredObjectMetadataItems.length;
objectMetadataItemsWithPermissionOverride.length ===
objectMetadataItemsThatCanHavePermission.length;
const handleAddRule = () => {
navigateSettings(SettingsPath.RoleAddObjectLevel, {
@@ -82,8 +65,8 @@ export const SettingsRolePermissionsObjectLevelSection = ({
};
const hasObjectPermissions =
isDefined(filteredObjectPermissions) &&
filteredObjectPermissions?.length > 0;
isDefined(objectMetadataItemsWithPermissionOverride) &&
objectMetadataItemsWithPermissionOverride?.length > 0;
return (
<Section>
@@ -93,16 +76,15 @@ export const SettingsRolePermissionsObjectLevelSection = ({
/>
<StyledTableRows>
{hasObjectPermissions ? (
filteredObjectPermissions?.map((objectPermission) => (
<SettingsRolePermissionsObjectLevelTableRow
key={objectPermission.objectMetadataId}
objectPermission={objectPermission}
objectMetadataItem={
objectMetadataMap[objectPermission.objectMetadataId]
}
roleId={roleId}
/>
))
objectMetadataItemsWithPermissionOverride.map(
(objectMetadataItem) => (
<SettingsRolePermissionsObjectLevelTableRow
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
),
)
) : (
<StyledNoOverride>
{t`No permissions have been set for individual objects.`}
@@ -0,0 +1,69 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
type SettingsRolePermissionsObjectLevelSeeFieldsValueForObjectProps = {
roleId: string;
objectMetadataItemId: string;
};
export const SettingsRolePermissionsObjectLevelSeeFieldsValueForObject = ({
roleId,
objectMetadataItemId,
}: SettingsRolePermissionsObjectLevelSeeFieldsValueForObjectProps) => {
const { t } = useLingui();
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
);
const numberOfRestrictableFieldMetadataItemsOnRead =
restrictableFieldMetadataItems.length;
const roleFieldPermissions =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermission) =>
fieldPermission.objectMetadataId === objectMetadataItemId,
) ?? [];
const numberOfRestrictedFieldMetadataItemsOnRead =
roleFieldPermissions.filter(
(fieldPermission) => fieldPermission.canReadFieldValue === false,
).length;
const canReadSome =
numberOfRestrictedFieldMetadataItemsOnRead > 0 &&
numberOfRestrictedFieldMetadataItemsOnRead <
numberOfRestrictableFieldMetadataItemsOnRead;
const canReadAll =
roleFieldPermissions.length === 0 ||
numberOfRestrictedFieldMetadataItemsOnRead === 0;
const { objectReadIsRestricted } = useObjectPermissionDerivedStates({
roleId,
objectMetadataItemId,
});
return (
<>
{objectReadIsRestricted
? '-'
: canReadAll
? t`All`
: canReadSome
? t`Some`
: t`No`}
</>
);
};
@@ -1,3 +1,4 @@
import { OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
@@ -9,9 +10,11 @@ type SettingsRolePermissionsObjectLevelTableHeaderProps = {
export const SettingsRolePermissionsObjectLevelTableHeader = ({
showPermissionsLabel = true,
}: SettingsRolePermissionsObjectLevelTableHeaderProps) => (
<TableRow gridAutoColumns="180px 1fr 1fr">
<TableRow gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}>
<TableHeader>{t`Object-Level`}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`Permissions` : ''}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`See Fields` : ''}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`Edit Fields` : ''}</TableHeader>
<TableHeader></TableHeader>
</TableRow>
);
@@ -1,5 +1,8 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelOverrideCellContainer } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCellContainer';
import { SettingsRolePermissionsObjectLevelSeeFieldsValueForObject } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSeeFieldsValueForObject';
import { SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject';
import { OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
@@ -10,7 +13,6 @@ import {
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { ObjectPermission } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledNameTableCell = styled(TableCell)`
@@ -25,23 +27,17 @@ const StyledNameLabel = styled.div`
`;
type SettingsRolePermissionsObjectLevelTableRowProps = {
objectPermission: ObjectPermission;
objectMetadataItem: ObjectMetadataItem;
roleId: string;
};
export const SettingsRolePermissionsObjectLevelTableRow = ({
objectPermission,
objectMetadataItem,
roleId,
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
const { getIcon } = useIcons();
const theme = useTheme();
if (!objectMetadataItem) {
throw new Error('Object metadata item not found');
}
const Icon = getIcon(objectMetadataItem.icon);
const objectLabelPlural = objectMetadataItem.labelPlural;
@@ -50,9 +46,9 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
<TableRow
to={getSettingsPath(SettingsPath.RoleObjectLevel, {
roleId: roleId,
objectMetadataId: objectPermission.objectMetadataId,
objectMetadataId: objectMetadataItem.id,
})}
gridAutoColumns="180px 1fr 1fr"
gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}
>
<StyledNameTableCell>
{!!Icon && (
@@ -68,11 +64,23 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
</StyledNameTableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelOverrideCellContainer
objectPermissions={objectPermission}
objectMetadataItem={objectMetadataItem}
roleId={roleId}
objectLabel={objectLabelPlural}
/>
</TableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelSeeFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell align={'right'}>
<IconChevronRight
size={theme.icon.size.md}
@@ -0,0 +1,70 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { filterUserFacingFieldMetadataItems } from '@/object-metadata/utils/filterUserFacingFieldMetadataItems';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
type SettingsRolePermissionsObjectLevelUpdateFieldsValueForObjectProps = {
roleId: string;
objectMetadataItemId: string;
};
export const SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject = ({
roleId,
objectMetadataItemId,
}: SettingsRolePermissionsObjectLevelUpdateFieldsValueForObjectProps) => {
const { t } = useLingui();
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
filterUserFacingFieldMetadataItems,
);
const numberOfRestrictableFieldMetadataItems =
restrictableFieldMetadataItems.length;
const roleFieldPermissions =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermission) =>
fieldPermission.objectMetadataId === objectMetadataItemId,
) ?? [];
const numberOfRestrictedFieldMetadataItemsOnUpdate =
roleFieldPermissions.filter(
(fieldPermission) => fieldPermission.canUpdateFieldValue === false,
).length;
const canUpdateSome =
numberOfRestrictedFieldMetadataItemsOnUpdate > 0 &&
numberOfRestrictedFieldMetadataItemsOnUpdate <
numberOfRestrictableFieldMetadataItems;
const canUpdateAll =
roleFieldPermissions.length === 0 ||
numberOfRestrictedFieldMetadataItemsOnUpdate === 0;
const { objectUpdateIsRestricted } = useObjectPermissionDerivedStates({
roleId,
objectMetadataItemId,
});
return (
<>
{objectUpdateIsRestricted
? '-'
: canUpdateAll
? t`All`
: canUpdateSome
? t`Some`
: t`No`}
</>
);
};
@@ -0,0 +1,2 @@
export const OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS =
'30% 20% 20% 20% 10%';
@@ -0,0 +1,153 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { filterUserFacingFieldMetadataItems } from '@/object-metadata/utils/filterUserFacingFieldMetadataItems';
import { StyledObjectFieldTableRow } from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTableAllHeaderRow } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/components/SettingsRolePermissionsObjectLevelObjectFieldPermissionTableAllHeaderRow';
import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/components/SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { OrderBy } from '@/types/OrderBy';
import { TextInput } from '@/ui/input/components/TextInput';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableHeaderText } from '@/ui/layout/table/components/TableHeaderText';
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { H2Title, IconEye, IconPencil, IconSearch } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { sortByProperty } from '~/utils/array/sortByProperty';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
import { turnOrderByIntoSort } from '~/utils/turnOrderByIntoSort';
export const SETTINGS_ROLE_PERMISSION_OBJECT_LEVEL_FIELD_PERMISSION_TABLE_ID =
'settings-role-permissions-object-level-object-field-permission';
const StyledSearchInput = styled(TextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export type SettingsRolePermissionsObjectLevelObjectFieldPermissionTableProps =
{
objectMetadataItem: ObjectMetadataItem;
roleId: string;
};
export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTable = ({
objectMetadataItem,
roleId,
}: SettingsRolePermissionsObjectLevelObjectFieldPermissionTableProps) => {
const theme = useTheme();
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
const sortedFieldByTable = useRecoilValue(
sortedFieldByTableFamilyState({
tableId: SETTINGS_ROLE_PERMISSION_OBJECT_LEVEL_FIELD_PERMISSION_TABLE_ID,
}),
);
const searchedFields = objectMetadataItem.fields.filter((fieldMetadataItem) =>
fieldMetadataItem.label
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase()),
);
const restrictableFieldMetadataItems = [
...searchedFields.filter(filterUserFacingFieldMetadataItems),
].sort(
sortByProperty(
'label',
turnOrderByIntoSort(
sortedFieldByTable?.orderBy ?? ('AscNullsFirst' satisfies OrderBy),
),
),
);
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const fieldPermissions =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermission) =>
fieldPermission.objectMetadataId === objectMetadataItem.id,
) ?? [];
const { cannotAllowFieldReadRestrict, cannotAllowFieldUpdateRestrict } =
useObjectPermissionDerivedStates({
roleId,
objectMetadataItemId: objectMetadataItem.id,
});
return (
<Section>
<H2Title
title={t`Fields Permissions`}
description={t`Ability to interact with this object's fields.`}
/>
<StyledSearchInput
instanceId="object-field-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a field...`}
value={searchTerm}
onChange={setSearchTerm}
/>
<Table>
<StyledObjectFieldTableRow>
<SortableTableHeader
fieldName={'label'}
label={t`Name`}
tableId={
SETTINGS_ROLE_PERMISSION_OBJECT_LEVEL_FIELD_PERMISSION_TABLE_ID
}
initialSort={{ fieldName: 'label', orderBy: 'AscNullsFirst' }}
/>
<TableHeader>
<TableHeaderText>{t`Data type`}</TableHeaderText>
</TableHeader>
{cannotAllowFieldReadRestrict ? (
<TableHeader></TableHeader>
) : (
<TableHeader align="center">
<TableHeaderText>{t`See`}</TableHeaderText>
<IconEye
size={theme.icon.size.sm}
stroke={theme.icon.stroke.md}
/>
</TableHeader>
)}
{cannotAllowFieldUpdateRestrict ? (
<TableHeader></TableHeader>
) : (
<TableHeader>
<TableHeaderText>{t`Edit`}</TableHeaderText>
<IconPencil
size={theme.icon.size.sm}
stroke={theme.icon.stroke.md}
/>
</TableHeader>
)}
</StyledObjectFieldTableRow>
<SettingsRolePermissionsObjectLevelObjectFieldPermissionTableAllHeaderRow
roleId={roleId}
objectMetadataItem={objectMetadataItem}
/>
{isNonEmptyArray(restrictableFieldMetadataItems) &&
restrictableFieldMetadataItems.map((fieldMetadataItem) => (
<SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
fieldPermissions={fieldPermissions}
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
))}
</Table>
</Section>
);
};
@@ -0,0 +1,127 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FIELD_LEVEL_PERMISSION_TABLE_GRID_TEMPLATE_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/constants/FieldLevelPermissionTableGridTemplateColumns';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { useRemoveReadOverrideOnAllFieldsOfObject } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRemoveReadOverrideOnAllFieldsOfObject';
import { useRemoveUpdateOverrideOnAllFieldsOfObject } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRemoveUpdateOverrideOnAllFieldsOfObject';
import { useRestrictReadOnAllFieldsOfObject } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRestrictReadOnAllFieldsOfObject';
import { useRestrictUpdateOnAllFieldsOfObject } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRestrictUpdateOnAllFieldsOfObject';
import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { Label } from 'twenty-ui/display';
const StyledSectionHeader = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
display: grid;
grid-template-columns: ${FIELD_LEVEL_PERMISSION_TABLE_GRID_TEMPLATE_COLUMNS};
height: ${({ theme }) => theme.spacing(6)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledCheckboxContainer = styled.div`
display: flex;
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTableAllHeaderRow =
({
roleId,
objectMetadataItem,
}: {
roleId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { t } = useLingui();
const [settingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const { cannotAllowFieldReadRestrict, cannotAllowFieldUpdateRestrict } =
useObjectPermissionDerivedStates({
roleId,
objectMetadataItemId: objectMetadataItem.id,
});
const fieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItem.id,
) ?? [];
const hasAnyRestrictionOnRead = fieldPermissionsForThisObject.some(
(fieldPermission) => fieldPermission.canReadFieldValue === false,
);
const hasAnyRestrictionOnUpdate = fieldPermissionsForThisObject.some(
(fieldPermission) => fieldPermission.canUpdateFieldValue === false,
);
const { restrictReadOnAllFieldsOfObject } =
useRestrictReadOnAllFieldsOfObject({ roleId });
const { removeReadOverrideOnAllFieldsOfObject } =
useRemoveReadOverrideOnAllFieldsOfObject({ roleId });
const { removeUpdateOverrideOnAllFieldsOfObject } =
useRemoveUpdateOverrideOnAllFieldsOfObject({ roleId });
const { restrictUpdateOnAllFieldsOfObject } =
useRestrictUpdateOnAllFieldsOfObject({ roleId });
const handleReadAllChange = () => {
if (hasAnyRestrictionOnRead) {
removeReadOverrideOnAllFieldsOfObject(objectMetadataItem);
} else {
restrictReadOnAllFieldsOfObject(objectMetadataItem);
}
};
const handleUpdateAllChange = () => {
if (hasAnyRestrictionOnUpdate) {
removeUpdateOverrideOnAllFieldsOfObject(objectMetadataItem);
} else {
restrictUpdateOnAllFieldsOfObject(objectMetadataItem);
}
};
return (
<>
<StyledSectionHeader>
<Label>{t`All`}</Label>
<div></div>
{cannotAllowFieldReadRestrict ? (
<div></div>
) : (
<StyledCheckboxContainer>
<OverridableCheckbox
disabled={false}
checked={true}
onChange={handleReadAllChange}
type={hasAnyRestrictionOnRead ? 'override' : 'default'}
/>
</StyledCheckboxContainer>
)}
{cannotAllowFieldUpdateRestrict ? (
<div></div>
) : (
<StyledCheckboxContainer>
<OverridableCheckbox
disabled={false}
checked={true}
onChange={handleUpdateAllChange}
type={hasAnyRestrictionOnUpdate ? 'override' : 'default'}
/>
</StyledCheckboxContainer>
)}
</StyledSectionHeader>
</>
);
};
@@ -0,0 +1,201 @@
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
import { SettingsObjectFieldDataType } from '@/settings/data-model/object-details/components/SettingsObjectFieldDataType';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { useUpsertFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useUpsertFieldPermissionInDraftRole';
import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { v4 } from 'uuid';
import { FieldPermission, RelationType } from '~/generated/graphql';
export const StyledObjectFieldTableRow = styled(TableRow)`
grid-template-columns: 180px 1fr 60px 60px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledNameLabel = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
type SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRowProps = {
fieldMetadataItem: FieldMetadataItem;
objectMetadataItem: ObjectMetadataItem;
fieldPermissions: FieldPermission[];
roleId: string;
};
export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow =
({
fieldMetadataItem,
fieldPermissions,
objectMetadataItem,
roleId,
}: SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRowProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(fieldMetadataItem.icon);
const getRelationMetadata = useGetRelationMetadata();
const { relationObjectMetadataItem, relationType } =
useMemo(
() => getRelationMetadata({ fieldMetadataItem }),
[fieldMetadataItem, getRelationMetadata],
) ?? {};
const fieldType = fieldMetadataItem.type;
const RelationIcon = relationType
? RELATION_TYPES[relationType].Icon
: undefined;
const fieldPermissionForThisFieldMetadataItem = fieldPermissions.find(
(fieldPermissionItem) =>
fieldPermissionItem.fieldMetadataId === fieldMetadataItem.id,
);
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const handleSeeChange = () => {
if (isDefined(fieldPermissionForThisFieldMetadataItem)) {
if (
fieldPermissionForThisFieldMetadataItem.canReadFieldValue === false
) {
upsertFieldPermissionInDraftRole({
...fieldPermissionForThisFieldMetadataItem,
canReadFieldValue: null,
});
} else {
upsertFieldPermissionInDraftRole({
...fieldPermissionForThisFieldMetadataItem,
canReadFieldValue: false,
canUpdateFieldValue: false,
});
}
} else {
upsertFieldPermissionInDraftRole({
id: v4(),
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canReadFieldValue: false,
canUpdateFieldValue: false,
roleId,
});
}
};
const handleUpdateChange = () => {
if (isDefined(fieldPermissionForThisFieldMetadataItem)) {
if (
fieldPermissionForThisFieldMetadataItem.canUpdateFieldValue === false
) {
upsertFieldPermissionInDraftRole({
...fieldPermissionForThisFieldMetadataItem,
canUpdateFieldValue: null,
canReadFieldValue: null,
});
} else {
upsertFieldPermissionInDraftRole({
...fieldPermissionForThisFieldMetadataItem,
canUpdateFieldValue: false,
});
}
} else {
upsertFieldPermissionInDraftRole({
id: v4(),
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canUpdateFieldValue: false,
roleId,
});
}
};
const hasRestriction = isDefined(fieldPermissionForThisFieldMetadataItem);
const isReadRestricted =
hasRestriction &&
fieldPermissionForThisFieldMetadataItem?.canReadFieldValue === false;
const isUpdateRestricted =
hasRestriction &&
fieldPermissionForThisFieldMetadataItem?.canUpdateFieldValue === false;
const { objectReadIsRestricted, objectUpdateIsRestricted } =
useObjectPermissionDerivedStates({
roleId,
objectMetadataItemId: objectMetadataItem.id,
});
return (
<StyledObjectFieldTableRow>
<StyledNameTableCell>
{!!Icon && (
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
<StyledNameLabel title={fieldMetadataItem.label}>
{fieldMetadataItem.label}
</StyledNameLabel>
</StyledNameTableCell>
<TableCell>
<SettingsObjectFieldDataType
Icon={RelationIcon}
label={
relationType === RelationType.MANY_TO_ONE
? relationObjectMetadataItem?.labelSingular
: relationObjectMetadataItem?.labelPlural
}
labelDetail={
fieldMetadataItem.settings?.type === 'percentage'
? '%'
: undefined
}
value={fieldType as SettingsFieldType}
/>
</TableCell>
{objectReadIsRestricted ? (
<TableCell />
) : (
<TableCell>
<OverridableCheckbox
disabled={false}
checked={true}
onChange={handleSeeChange}
type={isReadRestricted ? 'override' : 'default'}
/>
</TableCell>
)}
{objectUpdateIsRestricted ? (
<TableCell />
) : (
<TableCell align="left">
<OverridableCheckbox
disabled={false}
checked={true}
onChange={handleUpdateChange}
type={isUpdateRestricted ? 'override' : 'default'}
/>
</TableCell>
)}
</StyledObjectFieldTableRow>
);
};
@@ -0,0 +1,2 @@
export const FIELD_LEVEL_PERMISSION_TABLE_GRID_TEMPLATE_COLUMNS =
'180px 1fr 60px 60px';
@@ -0,0 +1,92 @@
import { renderHook } from '@testing-library/react';
import {
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL,
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL,
MOCK_OBJECT_PERMISSION_3_NULL_ALL,
MOCK_ROLE_ID_GRANTS_ALL,
MOCK_ROLE_ID_REVOKES_ALL,
rolesMockHookWrapper,
} from '~/testing/mock-data/roles/roles-mock';
import { useGetObjectPermissionDerivedStates } from '../useGetObjectPermissionDerivedStates';
describe('useGetObjectPermissionDerivedStates', () => {
it('should return expected for object that revoke permissions', () => {
const { result } = renderHook(
() =>
useGetObjectPermissionDerivedStates({
roleId: MOCK_ROLE_ID_GRANTS_ALL,
}),
{
wrapper: rolesMockHookWrapper,
},
);
const {
objectHasReadRevoked,
objectHasUpdateRevoked,
objectHasDeleteRevoked,
objectHasDestroyRevoked,
objectHasNoOverrideOnObjectPermission,
} = result.current.getObjectPermissionDerivedStates(
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL.objectMetadataId,
);
expect(objectHasReadRevoked).toEqual(true);
expect(objectHasUpdateRevoked).toEqual(true);
expect(objectHasDeleteRevoked).toEqual(true);
expect(objectHasDestroyRevoked).toEqual(true);
expect(objectHasNoOverrideOnObjectPermission).toEqual(false);
});
it('should return expected for object that grants permissions', () => {
const { result } = renderHook(
() =>
useGetObjectPermissionDerivedStates({
roleId: MOCK_ROLE_ID_REVOKES_ALL,
}),
{
wrapper: rolesMockHookWrapper,
},
);
const {
objectHasReadGranted,
objectHasUpdateGranted,
objectHasDeleteGranted,
objectHasDestroyGranted,
objectHasNoOverrideOnObjectPermission,
} = result.current.getObjectPermissionDerivedStates(
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL.objectMetadataId,
);
expect(objectHasReadGranted).toEqual(true);
expect(objectHasUpdateGranted).toEqual(true);
expect(objectHasDeleteGranted).toEqual(true);
expect(objectHasDestroyGranted).toEqual(true);
expect(objectHasNoOverrideOnObjectPermission).toEqual(false);
});
it('should return expected field permission only', () => {
const { result } = renderHook(
() =>
useGetObjectPermissionDerivedStates({
roleId: MOCK_ROLE_ID_GRANTS_ALL,
}),
{
wrapper: rolesMockHookWrapper,
},
);
const {
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
objectHasNoOverrideOnObjectPermission,
} = result.current.getObjectPermissionDerivedStates(
MOCK_OBJECT_PERMISSION_3_NULL_ALL.objectMetadataId,
);
expect(
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
).toEqual(true);
expect(objectHasNoOverrideOnObjectPermission).toEqual(true);
});
});
@@ -0,0 +1,193 @@
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useGetObjectPermissionDerivedStates = ({
roleId,
}: {
roleId: string;
}) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const getObjectPermissionDerivedStates = (objectMetadataItemId: string) => {
const objectPermission = settingsDraftRole.objectPermissions?.find(
(objectPermissionToFind) =>
objectPermissionToFind.objectMetadataId === objectMetadataItemId,
);
const isObjectPermissionDefined = isDefined(objectPermission);
const objectPermissionHasOnlyNullPermissions =
isObjectPermissionDefined &&
!isDefined(objectPermission.canReadObjectRecords) &&
!isDefined(objectPermission.canUpdateObjectRecords) &&
!isDefined(objectPermission.canSoftDeleteObjectRecords) &&
!isDefined(objectPermission.canDestroyObjectRecords);
const readIsRestrictedOnAllObjectsByDefault =
settingsDraftRole.canReadAllObjectRecords === false;
const updateIsRestrictedOnAllObjectsByDefault =
settingsDraftRole.canUpdateAllObjectRecords === false;
const deleteIsRestrictedOnAllObjectsByDefault =
settingsDraftRole.canSoftDeleteAllObjectRecords === false;
const destroyIsRestrictedOnAllObjectsByDefault =
settingsDraftRole.canDestroyAllObjectRecords === false;
const readIsAllowedOnAllObjectsByDefault =
settingsDraftRole.canReadAllObjectRecords === true;
const updateIsAllowedOnAllObjectsByDefault =
settingsDraftRole.canUpdateAllObjectRecords === true;
const deleteIsAllowedOnAllObjectsByDefault =
settingsDraftRole.canSoftDeleteAllObjectRecords === true;
const destroyIsAllowedOnAllObjectsByDefault =
settingsDraftRole.canDestroyAllObjectRecords === true;
const objectHasReadGranted =
readIsRestrictedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canReadObjectRecords === true;
const objectHasReadRevoked =
readIsAllowedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canReadObjectRecords === false;
const objectHasNoOverrideOnRead =
!isObjectPermissionDefined ||
(!objectHasReadGranted && !objectHasReadRevoked);
const objectHasUpdateGranted =
updateIsRestrictedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canUpdateObjectRecords === true;
const objectHasUpdateRevoked =
updateIsAllowedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canUpdateObjectRecords === false;
const objectHasNoOverrideOnUpdate =
!isObjectPermissionDefined ||
(!objectHasUpdateGranted && !objectHasUpdateRevoked);
const objectHasDeleteGranted =
deleteIsRestrictedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canSoftDeleteObjectRecords === true;
const objectHasDeleteRevoked =
deleteIsAllowedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canSoftDeleteObjectRecords === false;
const objectHasNoOverrideOnDelete =
!isObjectPermissionDefined ||
(!objectHasDeleteGranted && !objectHasDeleteRevoked);
const objectHasDestroyGranted =
destroyIsRestrictedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canDestroyObjectRecords === true;
const objectHasDestroyRevoked =
destroyIsAllowedOnAllObjectsByDefault &&
isObjectPermissionDefined &&
objectPermission.canDestroyObjectRecords === false;
const objectHasNoOverrideOnDestroy =
!isObjectPermissionDefined ||
(!objectHasDestroyGranted && !objectHasDestroyRevoked);
const objectHasNoOverrideOnObjectPermission =
objectHasNoOverrideOnRead &&
objectHasNoOverrideOnUpdate &&
objectHasNoOverrideOnDelete &&
objectHasNoOverrideOnDestroy;
const objectReadIsRestricted =
(readIsRestrictedOnAllObjectsByDefault && objectHasNoOverrideOnRead) ||
objectHasReadRevoked;
const objectUpdateIsRestricted =
(updateIsRestrictedOnAllObjectsByDefault &&
objectHasNoOverrideOnUpdate) ||
objectHasUpdateRevoked;
const cannotAllowFieldUpdateRestrict =
objectReadIsRestricted || objectUpdateIsRestricted;
const cannotAllowFieldReadRestrict = objectReadIsRestricted;
const canRestrictFieldRead = !cannotAllowFieldReadRestrict;
const canRestrictFieldUpdate = !cannotAllowFieldUpdateRestrict;
const fieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItemId,
);
const fieldPermissionsThatRevokeRead =
fieldPermissionsForThisObject?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.canReadFieldValue === false,
) ?? [];
const fieldPermissionsThatRevokeUpdate =
fieldPermissionsForThisObject?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.canUpdateFieldValue === false,
) ?? [];
const isThereAnyFieldPermissionThatRevokeRead =
fieldPermissionsThatRevokeRead.length > 0;
const isThereAnyFieldPermissionThatRevokeUpdate =
fieldPermissionsThatRevokeUpdate.length > 0;
const thereAreFieldPermissionsButTheyShouldntBeTakenIntoAccountBecauseObjectPermissionsDontAllowIt =
isThereAnyFieldPermissionThatRevokeRead &&
cannotAllowFieldReadRestrict &&
isThereAnyFieldPermissionThatRevokeUpdate &&
cannotAllowFieldUpdateRestrict;
const objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount =
objectHasNoOverrideOnObjectPermission &&
((isThereAnyFieldPermissionThatRevokeRead && canRestrictFieldRead) ||
(isThereAnyFieldPermissionThatRevokeUpdate && canRestrictFieldUpdate));
const objectHasOverrideOnObjectPermissions =
!objectHasNoOverrideOnObjectPermission;
return {
objectReadIsRestricted,
objectUpdateIsRestricted,
cannotAllowFieldUpdateRestrict,
cannotAllowFieldReadRestrict,
objectHasReadRevoked,
objectHasUpdateRevoked,
objectHasDeleteRevoked,
objectHasDestroyRevoked,
objectHasReadGranted,
objectHasUpdateGranted,
objectHasDeleteGranted,
objectHasDestroyGranted,
objectHasNoOverrideOnObjectPermission,
thereAreFieldPermissionsButTheyShouldntBeTakenIntoAccountBecauseObjectPermissionsDontAllowIt,
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
objectPermissionHasOnlyNullPermissions,
objectHasOverrideOnObjectPermissions,
};
};
return {
getObjectPermissionDerivedStates,
};
};
@@ -0,0 +1,44 @@
import { useGetObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates';
export const useObjectPermissionDerivedStates = ({
roleId,
objectMetadataItemId,
}: {
roleId: string;
objectMetadataItemId: string;
}) => {
const { getObjectPermissionDerivedStates } =
useGetObjectPermissionDerivedStates({
roleId,
});
const {
objectReadIsRestricted,
objectUpdateIsRestricted,
cannotAllowFieldUpdateRestrict,
cannotAllowFieldReadRestrict,
objectHasUpdateGranted,
objectHasUpdateRevoked,
objectHasReadGranted,
objectHasReadRevoked,
objectHasNoOverrideOnObjectPermission,
thereAreFieldPermissionsButTheyShouldntBeTakenIntoAccountBecauseObjectPermissionsDontAllowIt,
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
objectPermissionHasOnlyNullPermissions,
} = getObjectPermissionDerivedStates(objectMetadataItemId);
return {
objectReadIsRestricted,
objectUpdateIsRestricted,
cannotAllowFieldUpdateRestrict,
cannotAllowFieldReadRestrict,
objectHasUpdateGranted,
objectHasUpdateRevoked,
objectHasReadGranted,
objectHasReadRevoked,
objectHasNoOverrideOnObjectPermission,
thereAreFieldPermissionsButTheyShouldntBeTakenIntoAccountBecauseObjectPermissionsDontAllowIt,
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
objectPermissionHasOnlyNullPermissions,
};
};
@@ -0,0 +1,26 @@
import { getRoleWithRemovedFieldPermission } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/utils/getRoleWithRemovedFieldPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilCallback } from 'recoil';
export const useRemoveFieldPermissionInDraftRole = () => {
const removeFieldPermissionInDraftRole = useRecoilCallback(
({ set, snapshot }) =>
(roleId: string, fieldPermissionFieldMetadataId: string) => {
const currentSettingsDraftRole = snapshot
.getLoadable(settingsDraftRoleFamilyState(roleId))
.getValue();
const newRole = getRoleWithRemovedFieldPermission(
currentSettingsDraftRole,
fieldPermissionFieldMetadataId,
);
set(settingsDraftRoleFamilyState(roleId), newRole);
},
[],
);
return {
removeFieldPermissionInDraftRole,
};
};
@@ -0,0 +1,38 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useUpsertFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useUpsertFieldPermissionInDraftRole';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
export const useRemoveReadOverrideOnAllFieldsOfObject = ({
roleId,
}: {
roleId: string;
}) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const removeReadOverrideOnAllFieldsOfObject = (
objectMetadataItem: ObjectMetadataItem,
) => {
const existingFieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItem.id,
) ?? [];
for (const fieldPermissionToChange of existingFieldPermissionsForThisObject) {
upsertFieldPermissionInDraftRole({
...fieldPermissionToChange,
canReadFieldValue: null,
});
}
};
return {
removeReadOverrideOnAllFieldsOfObject,
};
};
@@ -0,0 +1,39 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useUpsertFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useUpsertFieldPermissionInDraftRole';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
export const useRemoveUpdateOverrideOnAllFieldsOfObject = ({
roleId,
}: {
roleId: string;
}) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const removeUpdateOverrideOnAllFieldsOfObject = (
objectMetadataItem: ObjectMetadataItem,
) => {
const existingFieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItem.id,
) ?? [];
for (const fieldPermissionToChange of existingFieldPermissionsForThisObject) {
upsertFieldPermissionInDraftRole({
...fieldPermissionToChange,
canUpdateFieldValue: null,
canReadFieldValue: null,
});
}
};
return {
removeUpdateOverrideOnAllFieldsOfObject,
};
};
@@ -0,0 +1,75 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { filterUserFacingFieldMetadataItems } from '@/object-metadata/utils/filterUserFacingFieldMetadataItems';
import { useUpsertFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useUpsertFieldPermissionInDraftRole';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
export const useRestrictReadOnAllFieldsOfObject = ({
roleId,
}: {
roleId: string;
}) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const restrictReadOnAllFieldsOfObject = (
objectMetadataItem: ObjectMetadataItem,
) => {
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
filterUserFacingFieldMetadataItems,
);
const existingFieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItem.id,
) ?? [];
if (existingFieldPermissionsForThisObject.length === 0) {
for (const fieldMetadataItem of restrictableFieldMetadataItems) {
upsertFieldPermissionInDraftRole({
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canUpdateFieldValue: false,
canReadFieldValue: false,
id: v4(),
roleId,
});
}
} else {
for (const fieldMetadataItem of restrictableFieldMetadataItems) {
const foundFieldPermission = existingFieldPermissionsForThisObject.find(
(fieldPermissionToFind) =>
fieldPermissionToFind.fieldMetadataId === fieldMetadataItem.id,
);
if (isDefined(foundFieldPermission)) {
upsertFieldPermissionInDraftRole({
...foundFieldPermission,
canReadFieldValue: false,
canUpdateFieldValue: false,
});
} else {
upsertFieldPermissionInDraftRole({
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canReadFieldValue: false,
canUpdateFieldValue: false,
id: v4(),
roleId,
});
}
}
}
};
return {
restrictReadOnAllFieldsOfObject,
};
};
@@ -0,0 +1,78 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { filterUserFacingFieldMetadataItems } from '@/object-metadata/utils/filterUserFacingFieldMetadataItems';
import { useUpsertFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useUpsertFieldPermissionInDraftRole';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
export const useRestrictUpdateOnAllFieldsOfObject = ({
roleId,
}: {
roleId: string;
}) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const restrictUpdateOnAllFieldsOfObject = (
objectMetadataItem: ObjectMetadataItem,
) => {
const existingFieldPermissionsForThisObject =
settingsDraftRole.fieldPermissions?.filter(
(fieldPermissionToFilter) =>
fieldPermissionToFilter.objectMetadataId === objectMetadataItem.id,
) ?? [];
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
filterUserFacingFieldMetadataItems,
);
const shouldCreateUpdatePermissionForAllFields =
existingFieldPermissionsForThisObject.length === 0;
if (shouldCreateUpdatePermissionForAllFields) {
for (const fieldMetadataItem of restrictableFieldMetadataItems) {
upsertFieldPermissionInDraftRole({
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canUpdateFieldValue: false,
canReadFieldValue: null,
id: v4(),
roleId,
});
}
} else {
for (const fieldMetadataItem of restrictableFieldMetadataItems) {
const alreadyExistingFieldPermission =
existingFieldPermissionsForThisObject.find(
(fieldPermissionToFind) =>
fieldPermissionToFind.fieldMetadataId === fieldMetadataItem.id,
);
if (isDefined(alreadyExistingFieldPermission)) {
upsertFieldPermissionInDraftRole({
...alreadyExistingFieldPermission,
canUpdateFieldValue: false,
});
} else {
upsertFieldPermissionInDraftRole({
fieldMetadataId: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
canUpdateFieldValue: false,
canReadFieldValue: null,
id: v4(),
roleId,
});
}
}
}
};
return {
restrictUpdateOnAllFieldsOfObject,
};
};
@@ -0,0 +1,25 @@
import { getRoleWithUpsertedFieldPermission } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/utils/getRoleWithUpsertedFieldPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useSetRecoilState } from 'recoil';
import { FieldPermission } from '~/generated/graphql';
export const useUpsertFieldPermissionInDraftRole = (roleId: string) => {
const setSettingsDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const upsertFieldPermissionInDraftRole = (
fieldPermissionToUpsert: FieldPermission,
) => {
setSettingsDraftRole((currentSettingsDraftRole) =>
getRoleWithUpsertedFieldPermission(
currentSettingsDraftRole,
fieldPermissionToUpsert,
),
);
};
return {
upsertFieldPermissionInDraftRole,
};
};
@@ -0,0 +1,26 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { Role } from '~/generated/graphql';
export const getRoleWithRemovedFieldPermission = (
role: Role,
fieldPermissionFieldMetadataId: string,
) => {
return produce(role, (draftRole) => {
if (!isNonEmptyArray(draftRole.fieldPermissions)) {
return;
}
const indexOfExistingFieldPermission = draftRole.fieldPermissions.findIndex(
(fieldPermissionToFind) =>
fieldPermissionToFind.fieldMetadataId ===
fieldPermissionFieldMetadataId,
);
if (indexOfExistingFieldPermission > -1) {
draftRole.fieldPermissions.splice(indexOfExistingFieldPermission, 1);
}
return draftRole;
});
};
@@ -0,0 +1,31 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { FieldPermission, Role } from '~/generated/graphql';
export const getRoleWithUpsertedFieldPermission = (
role: Role,
fieldPermissionToUpsert: FieldPermission,
) => {
return produce(role, (draftRole) => {
if (!isNonEmptyArray(draftRole.fieldPermissions)) {
draftRole.fieldPermissions = [fieldPermissionToUpsert];
return;
}
const indexOfExistingFieldPermission = draftRole.fieldPermissions.findIndex(
(fieldPermissionToFind) =>
fieldPermissionToFind.fieldMetadataId ===
fieldPermissionToUpsert.fieldMetadataId,
);
if (indexOfExistingFieldPermission > -1) {
draftRole.fieldPermissions[indexOfExistingFieldPermission] =
fieldPermissionToUpsert;
} else {
draftRole.fieldPermissions.push(fieldPermissionToUpsert);
}
return draftRole;
});
};
@@ -0,0 +1,71 @@
import { FieldPermission, Role } from '~/generated-metadata/graphql';
import { getRoleWithRemovedFieldPermission } from '../getRoleWithRemovedFieldPermission';
const BASE_FIELD_PERMISSION: FieldPermission = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: 'object-metadata-id-1',
roleId: '1',
canReadFieldValue: false,
canUpdateFieldValue: false,
};
const BASE_ROLE_MOCK: Role = {
canAccessAllTools: false,
canDestroyAllObjectRecords: true,
canReadAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canUpdateAllSettings: true,
id: '1',
isEditable: true,
label: 'Role base',
workspaceMembers: [],
fieldPermissions: [BASE_FIELD_PERMISSION],
};
describe('getRoleWithRemovedFieldPermission', () => {
it('should remove field permission with given fieldMetadataId', () => {
const role: Role = {
...BASE_ROLE_MOCK,
};
expect(role.fieldPermissions).toBeDefined();
const updatedRole = getRoleWithRemovedFieldPermission(
role,
BASE_FIELD_PERMISSION.fieldMetadataId,
);
expect(updatedRole.fieldPermissions).toBeDefined();
expect(updatedRole.fieldPermissions?.length).toBe(0);
});
it('should not remove not found fieldPermission', () => {
const role: Role = {
...BASE_ROLE_MOCK,
};
expect(role.fieldPermissions?.length).toBe(1);
const updatedRole = getRoleWithRemovedFieldPermission(role, 'unknown-id');
expect(updatedRole.fieldPermissions?.length).toBe(1);
});
it('should not remove if no fieldPermission property', () => {
const role: Role = {
...BASE_ROLE_MOCK,
fieldPermissions: undefined,
};
expect(role.fieldPermissions).not.toBeDefined();
const updatedRole = getRoleWithRemovedFieldPermission(
role,
BASE_FIELD_PERMISSION.fieldMetadataId,
);
expect(updatedRole.fieldPermissions).not.toBeDefined();
});
});
@@ -0,0 +1,106 @@
import { FieldPermission, Role } from '~/generated-metadata/graphql';
import { getRoleWithUpsertedFieldPermission } from '../getRoleWithUpsertedFieldPermission';
const BASE_ROLE_MOCK: Role = {
canAccessAllTools: false,
canDestroyAllObjectRecords: true,
canReadAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canUpdateAllSettings: true,
id: '1',
isEditable: true,
label: 'Role base',
workspaceMembers: [],
};
const BASE_FIELD_PERMISSION_TO_UPSERT: FieldPermission = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: 'object-metadata-id-1',
roleId: '1',
canReadFieldValue: false,
canUpdateFieldValue: false,
};
describe('getRoleWithUpsertedFieldPermission', () => {
it('should work with undefined fieldPermission property on role', () => {
const role: Role = {
...BASE_ROLE_MOCK,
};
const fieldPermissionToUpsert: FieldPermission = {
...BASE_FIELD_PERMISSION_TO_UPSERT,
};
expect(role.fieldPermissions).toBeUndefined();
const updatedRole = getRoleWithUpsertedFieldPermission(
role,
fieldPermissionToUpsert,
);
expect(updatedRole.fieldPermissions).toBeDefined();
expect(updatedRole.fieldPermissions).toContain(fieldPermissionToUpsert);
});
it('should work with existing fieldPermission', () => {
const baseFieldPermission: FieldPermission = {
...BASE_FIELD_PERMISSION_TO_UPSERT,
};
const role: Role = {
...BASE_ROLE_MOCK,
fieldPermissions: [baseFieldPermission],
};
expect(role.fieldPermissions).toBeDefined();
expect(role.fieldPermissions).toContain(baseFieldPermission);
const fieldPermissionToUpsert: FieldPermission = {
...BASE_FIELD_PERMISSION_TO_UPSERT,
canReadFieldValue: undefined,
canUpdateFieldValue: undefined,
};
const updatedRole = getRoleWithUpsertedFieldPermission(
role,
fieldPermissionToUpsert,
);
expect(updatedRole.fieldPermissions).toContain(fieldPermissionToUpsert);
expect(updatedRole.fieldPermissions?.[0].canReadFieldValue).not.toEqual(
role.fieldPermissions?.[0].canReadFieldValue,
);
expect(updatedRole.fieldPermissions?.[0].canUpdateFieldValue).not.toEqual(
role.fieldPermissions?.[0].canUpdateFieldValue,
);
});
it('should push fieldPermission in existing array', () => {
const role: Role = {
...BASE_ROLE_MOCK,
fieldPermissions: [BASE_FIELD_PERMISSION_TO_UPSERT],
};
expect(role.fieldPermissions).toBeDefined();
expect(role.fieldPermissions?.length).toBe(1);
const fieldPermissionToPush: FieldPermission = {
...BASE_FIELD_PERMISSION_TO_UPSERT,
fieldMetadataId: 'field-metadata-id-2',
id: '2',
};
const updatedRole = getRoleWithUpsertedFieldPermission(
role,
fieldPermissionToPush,
);
expect(updatedRole.fieldPermissions).toContain(fieldPermissionToPush);
expect(updatedRole.fieldPermissions).toContain(
BASE_FIELD_PERMISSION_TO_UPSERT,
);
expect(updatedRole.fieldPermissions?.length).toBe(2);
});
});
@@ -0,0 +1,32 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { ObjectPermission, Role } from '~/generated/graphql';
export const getRoleWithUpsertedObjectPermission = (
role: Role,
objectPermissionToUpsert: ObjectPermission,
) => {
return produce(role, (draftRole) => {
if (!isNonEmptyArray(draftRole.objectPermissions)) {
draftRole.objectPermissions = [objectPermissionToUpsert];
return;
}
const indexOfExistingObjectPermission =
draftRole.objectPermissions.findIndex(
(objectPermissionToFind) =>
objectPermissionToFind.objectMetadataId ===
objectPermissionToUpsert.objectMetadataId,
);
if (indexOfExistingObjectPermission > -1) {
draftRole.objectPermissions[indexOfExistingObjectPermission] =
objectPermissionToUpsert;
} else {
draftRole.objectPermissions.push(objectPermissionToUpsert);
}
return draftRole;
});
};
@@ -0,0 +1,34 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useGetObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates';
import { useCallback } from 'react';
export const useFilterObjectMetadataItemsWithPermissionOverride = ({
roleId,
}: {
roleId: string;
}) => {
const { getObjectPermissionDerivedStates } =
useGetObjectPermissionDerivedStates({
roleId,
});
const filterObjectMetadataItemsWithPermissionOverride = useCallback(
(objectMetadataItem: ObjectMetadataItem) => {
const {
objectHasOverrideOnObjectPermissions,
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount,
} = getObjectPermissionDerivedStates(objectMetadataItem.id);
const hasOverride =
objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount ||
objectHasOverrideOnObjectPermissions;
return hasOverride;
},
[getObjectPermissionDerivedStates],
);
return {
filterObjectMetadataItemsWithPermissionOverride,
};
};
@@ -0,0 +1,16 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
export const useObjectMetadataItemsThatCanHavePermission = () => {
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const objectMetadataItemsThatCanHavePermission = objectMetadataItems.filter(
(objectMetadataItem) =>
!isWorkflowRelatedObjectMetadata(objectMetadataItem.nameSingular),
);
return {
objectMetadataItemsThatCanHavePermission,
};
};
@@ -0,0 +1,63 @@
import { useUpsertObjectPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useUpsertObjectPermissionInDraftRole';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
export const useUpsertObjectPermission = ({ roleId }: { roleId: string }) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const { upsertObjectPermissionInDraftRole } =
useUpsertObjectPermissionInDraftRole(roleId);
const upsertObjectPermission = (
objectMetadataItemId: string,
permissionKey: SettingsRoleObjectPermissionKey,
value: boolean | null,
) => {
const existingObjectPermission = settingsDraftRole.objectPermissions?.find(
(objectPermissionToFind) =>
objectPermissionToFind.objectMetadataId === objectMetadataItemId,
);
const newPermissions = { [permissionKey]: value };
const isHigherPermission =
permissionKey === 'canUpdateObjectRecords' ||
permissionKey === 'canSoftDeleteObjectRecords' ||
permissionKey === 'canDestroyObjectRecords';
if (isHigherPermission && value !== false) {
newPermissions.canReadObjectRecords = value;
}
if (permissionKey === 'canReadObjectRecords' && !value) {
newPermissions.canUpdateObjectRecords = false;
newPermissions.canSoftDeleteObjectRecords = false;
newPermissions.canDestroyObjectRecords = false;
}
if (!isDefined(existingObjectPermission)) {
const newObjectPermission = {
objectMetadataId: objectMetadataItemId,
...newPermissions,
} satisfies ObjectPermission;
upsertObjectPermissionInDraftRole(newObjectPermission);
} else {
const updatedObjectPermission = {
...existingObjectPermission,
...newPermissions,
};
upsertObjectPermissionInDraftRole(updatedObjectPermission);
}
};
return {
upsertObjectPermission,
};
};
@@ -0,0 +1,25 @@
import { getRoleWithUpsertedObjectPermission } from '@/settings/roles/role-permissions/object-level-permissions/hooks/getRoleWithUpsertedObjectPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useSetRecoilState } from 'recoil';
import { ObjectPermission } from '~/generated/graphql';
export const useUpsertObjectPermissionInDraftRole = (roleId: string) => {
const setSettingsDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const upsertObjectPermissionInDraftRole = (
objectPermissionToUpsert: ObjectPermission,
) => {
setSettingsDraftRole((currentSettingsDraftRole) =>
getRoleWithUpsertedObjectPermission(
currentSettingsDraftRole,
objectPermissionToUpsert,
),
);
};
return {
upsertObjectPermissionInDraftRole,
};
};
@@ -1,5 +1,6 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTable } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/components/SettingsRolePermissionsObjectLevelObjectFieldPermissionTable';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
@@ -70,6 +71,10 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
<SettingsRolePermissionsObjectLevelObjectFieldPermissionTable
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
@@ -1,12 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { SettingsRolePermissionsObjectLevelPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
@@ -28,7 +27,7 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
roleId,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
@@ -37,83 +36,29 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
(permission) => permission.objectMetadataId === objectMetadataItem.id,
);
if (!settingsDraftRoleObjectPermissions) {
return null;
}
const objectLabelPlural = objectMetadataItem.labelPlural;
const updateObjectPermission = (
permissionKey: SettingsRoleObjectPermissionKey,
value: boolean | null,
) => {
setSettingsDraftRole((currentRole) => {
if (!currentRole.objectPermissions) {
return currentRole;
}
const updatedPermissions = currentRole.objectPermissions.map((perm) => {
if (perm.objectMetadataId !== objectMetadataItem.id) {
return perm;
}
const newPerms = { ...perm, [permissionKey]: value };
const isHigherPermission =
permissionKey === 'canUpdateObjectRecords' ||
permissionKey === 'canSoftDeleteObjectRecords' ||
permissionKey === 'canDestroyObjectRecords';
if (isHigherPermission && value !== false) {
newPerms.canReadObjectRecords = value;
}
if (permissionKey === 'canReadObjectRecords' && !value) {
newPerms.canUpdateObjectRecords = false;
newPerms.canSoftDeleteObjectRecords = false;
newPerms.canDestroyObjectRecords = false;
}
return newPerms;
});
return { ...currentRole, objectPermissions: updatedPermissions };
});
};
const objectPermissionsConfig: SettingsRolePermissionsObjectLevelPermission[] =
[
{
key: 'canReadObjectRecords',
label: t`See ${objectLabelPlural}`,
value: settingsDraftRoleObjectPermissions.canReadObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canReadObjectRecords', value);
},
value: settingsDraftRoleObjectPermissions?.canReadObjectRecords,
},
{
key: 'canUpdateObjectRecords',
label: t`Edit ${objectLabelPlural}`,
value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canUpdateObjectRecords', value);
},
value: settingsDraftRoleObjectPermissions?.canUpdateObjectRecords,
},
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete ${objectLabelPlural}`,
value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canSoftDeleteObjectRecords', value);
},
value: settingsDraftRoleObjectPermissions?.canSoftDeleteObjectRecords,
},
{
key: 'canDestroyObjectRecords',
label: t`Destroy ${objectLabelPlural}`,
value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canDestroyObjectRecords', value);
},
value: settingsDraftRoleObjectPermissions?.canDestroyObjectRecords,
},
];
@@ -135,6 +80,7 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
settingsDraftRoleObjectPermissions
}
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
))}
</StyledTableRows>
@@ -1,3 +1,4 @@
import { useUpsertObjectPermission } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useUpsertObjectPermission';
import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox';
import { objectPermissionKeyToHumanReadable } from '@/settings/roles/role-permissions/object-level-permissions/utils/objectPermissionKeyToHumanReadableText';
import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
@@ -55,14 +56,16 @@ const StyledCheckboxCell = styled(TableCell)`
type OverridableCheckboxType = 'no_cta' | 'default' | 'override';
type SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps = {
objectMetadataItemId: string;
permission: SettingsRolePermissionsObjectLevelPermission;
isEditable: boolean;
settingsDraftRoleObjectPermissions: ObjectPermission;
settingsDraftRoleObjectPermissions: ObjectPermission | undefined;
roleId: string;
};
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
({
objectMetadataItemId,
permission,
isEditable,
settingsDraftRoleObjectPermissions,
@@ -78,7 +81,7 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
const settingsDraftRoleObjectPermissionValue =
settingsDraftRoleObjectPermissions[
settingsDraftRoleObjectPermissions?.[
permission.key as keyof ObjectPermission
];
@@ -117,15 +120,23 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
checkboxType = 'default';
}
const { upsertObjectPermission } = useUpsertObjectPermission({
roleId,
});
const handleCheckboxChange = () => {
if (!isEditable) return;
if (checkboxType === 'default') {
permission.setValue(false);
upsertObjectPermission(objectMetadataItemId, permission.key, false);
} else if (checkboxType === 'override') {
permission.setValue(null);
upsertObjectPermission(objectMetadataItemId, permission.key, null);
} else if (checkboxType === 'no_cta') {
permission.setValue(!isChecked);
upsertObjectPermission(
objectMetadataItemId,
permission.key,
!isChecked,
);
}
};
@@ -1,31 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission, Role } from '~/generated-metadata/graphql';
export const hasPermissionOverride = (
objectPermission: ObjectPermission,
settingsDraftRole: Role,
) => {
const permissionChecks = [
{
permission: objectPermission.canReadObjectRecords,
globalPermission: settingsDraftRole.canReadAllObjectRecords,
},
{
permission: objectPermission.canUpdateObjectRecords,
globalPermission: settingsDraftRole.canUpdateAllObjectRecords,
},
{
permission: objectPermission.canSoftDeleteObjectRecords,
globalPermission: settingsDraftRole.canSoftDeleteAllObjectRecords,
},
{
permission: objectPermission.canDestroyObjectRecords,
globalPermission: settingsDraftRole.canDestroyAllObjectRecords,
},
];
return permissionChecks.some(
({ permission, globalPermission }) =>
isDefined(permission) && permission !== globalPermission,
);
};
@@ -65,6 +65,7 @@ export const SettingsRolePermissionsObjectsTableRow = ({
const handleRowClick = () => {
if (isDisabled) return;
permission.setValue(!permission.value);
};
@@ -1,16 +1,29 @@
import { ReactNode } from 'react';
import { ObjectPermission } from '~/generated/graphql';
export type SettingsRolePermissionsObjectPermission = {
key: string;
key: keyof Pick<
ObjectPermission,
| 'canDestroyObjectRecords'
| 'canReadObjectRecords'
| 'canSoftDeleteObjectRecords'
| 'canUpdateObjectRecords'
>;
label: string | ReactNode;
value?: boolean;
setValue: (value: boolean) => void;
grantedBy?: number;
revokedBy?: number;
setValue: (newValue: boolean) => void;
};
export type SettingsRolePermissionsObjectLevelPermission = {
key: string;
key: keyof Pick<
ObjectPermission,
| 'canDestroyObjectRecords'
| 'canReadObjectRecords'
| 'canSoftDeleteObjectRecords'
| 'canUpdateObjectRecords'
>;
label: string | ReactNode;
value?: boolean | null;
setValue: (value: boolean | null) => void;
};
@@ -1,13 +1,12 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment';
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
import { SettingsRoleSettings } from '@/settings/roles/role-settings/components/SettingsRoleSettings';
import { SettingsRoleLabelContainer } from '@/settings/roles/role/components/SettingsRoleLabelContainer';
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
import { useSaveDraftRoleToDB } from '@/settings/roles/role/hooks/useSaveDraftRoleToDB';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
@@ -17,19 +16,12 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { getOperationName } from '@apollo/client/utilities';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
import {
Role,
useCreateOneRoleMutation,
useUpdateOneRoleMutation,
useUpsertObjectPermissionsMutation,
useUpsertPermissionFlagsMutation,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getDirtyFields } from '~/utils/getDirtyFields';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@@ -40,18 +32,6 @@ type SettingsRoleProps = {
isCreateMode: boolean;
};
const ROLE_BASIC_KEYS: Array<keyof Role> = [
'label',
'description',
'icon',
'canUpdateAllSettings',
'canAccessAllTools',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
];
export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
@@ -60,15 +40,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const navigateSettings = useNavigateSettings();
const [createRole] = useCreateOneRoleMutation();
const [updateRole] = useUpdateOneRoleMutation();
const [upsertPermissionFlags] = useUpsertPermissionFlagsMutation();
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
const [isSaving, setIsSaving] = useState(false);
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
@@ -83,9 +56,10 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const { enqueueErrorSnackBar } = useSnackBar();
if (!isDefined(settingsRolesIsLoading)) {
return <></>;
}
const { saveDraftRoleToDB } = useSaveDraftRoleToDB({
isCreateMode,
roleId,
});
const isRoleEditable = settingsDraftRole.isEditable;
@@ -136,152 +110,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
}
try {
if (isCreateMode) {
const { data } = await createRole({
variables: {
createRoleInput: {
id: roleId,
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canAccessAllTools: settingsDraftRole.canAccessAllTools,
canReadAllObjectRecords:
settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
if (!data) {
return;
}
if (isDefined(dirtyFields.permissionFlags)) {
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId: data.createOneRole.id,
permissionFlagKeys:
settingsDraftRole.permissionFlags?.map(
(permissionFlag) => permissionFlag.flag,
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.objectPermissions)) {
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId: data.createOneRole.id,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords:
objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.workspaceMembers)) {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
}
navigateSettings(SettingsPath.RoleDetail, {
roleId: data.createOneRole.id,
});
} else {
if (isDefined(dirtyFields.permissionFlags)) {
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId: roleId,
permissionFlagKeys:
settingsDraftRole.permissionFlags?.map(
(permissionFlag) => permissionFlag.flag,
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (ROLE_BASIC_KEYS.some((key) => key in dirtyFields)) {
await updateRole({
variables: {
updateRoleInput: {
id: roleId,
update: {
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canAccessAllTools: settingsDraftRole.canAccessAllTools,
canReadAllObjectRecords:
settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
},
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.objectPermissions)) {
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId: roleId,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords:
objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
}
await saveDraftRoleToDB();
await loadCurrentUser();
} finally {
@@ -289,6 +118,10 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
}
};
if (!isDefined(settingsRolesIsLoading)) {
return <></>;
}
return (
<SubMenuTopBarContainer
title={<SettingsRoleLabelContainer roleId={roleId} />}
@@ -0,0 +1,286 @@
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { useRemoveFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRemoveFieldPermissionInDraftRole';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
useCreateOneRoleMutation,
useUpdateOneRoleMutation,
useUpsertFieldPermissionsMutation,
useUpsertObjectPermissionsMutation,
useUpsertPermissionFlagsMutation,
} from '~/generated-metadata/graphql';
import { Role } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getDirtyFields } from '~/utils/getDirtyFields';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
const ROLE_BASIC_KEYS: Array<keyof Role> = [
'label',
'description',
'icon',
'canUpdateAllSettings',
'canAccessAllTools',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
];
export const useSaveDraftRoleToDB = ({
roleId,
isCreateMode,
}: {
roleId: string;
isCreateMode: boolean;
}) => {
const [createRole] = useCreateOneRoleMutation();
const [updateRole] = useUpdateOneRoleMutation();
const [upsertPermissionFlags] = useUpsertPermissionFlagsMutation();
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
const [upsertFieldPermissions] = useUpsertFieldPermissionsMutation();
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
const navigateSettings = useNavigateSettings();
const settingsPersistedRole = useRecoilValue(
settingsPersistedRoleFamilyState(roleId),
);
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const dirtyFields = getDirtyFields(settingsDraftRole, settingsPersistedRole);
const fieldPermissionsThatShouldntBeCreatedBecauseTheyAreUseless =
settingsDraftRole.fieldPermissions?.filter((fieldPermissionToFilter) => {
const fieldPermissionDoesntExistYetInPersistedFieldPermissions =
!settingsPersistedRole?.fieldPermissions?.some(
(persistedFieldPermissionToFilter) =>
persistedFieldPermissionToFilter.fieldMetadataId ===
fieldPermissionToFilter.fieldMetadataId,
);
return (
fieldPermissionToFilter.canReadFieldValue !== false &&
fieldPermissionToFilter.canUpdateFieldValue !== false &&
fieldPermissionDoesntExistYetInPersistedFieldPermissions
);
});
const fieldPermissionsToUpsert =
dirtyFields.fieldPermissions?.filter(
(dirtyFieldPermissionToFilter) =>
!fieldPermissionsThatShouldntBeCreatedBecauseTheyAreUseless?.some(
(fieldPermissionThatShouldntBeCreatedToFilter) =>
dirtyFieldPermissionToFilter.fieldMetadataId ===
fieldPermissionThatShouldntBeCreatedToFilter.fieldMetadataId,
),
) ?? [];
const { removeFieldPermissionInDraftRole } =
useRemoveFieldPermissionInDraftRole();
const saveDraftRoleToDB = async () => {
if (
isNonEmptyArray(
fieldPermissionsThatShouldntBeCreatedBecauseTheyAreUseless,
)
) {
for (const fieldPermissionToRemoveInDraftRole of fieldPermissionsThatShouldntBeCreatedBecauseTheyAreUseless) {
removeFieldPermissionInDraftRole(
roleId,
fieldPermissionToRemoveInDraftRole.fieldMetadataId,
);
}
}
if (isCreateMode) {
const { data } = await createRole({
variables: {
createRoleInput: {
id: roleId,
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canAccessAllTools: settingsDraftRole.canAccessAllTools,
canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
} satisfies Partial<Role>,
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
if (!data) {
return;
}
if (isDefined(dirtyFields.permissionFlags)) {
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId: data.createOneRole.id,
permissionFlagKeys:
settingsDraftRole.permissionFlags?.map(
(permissionFlag) => permissionFlag.flag,
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.objectPermissions)) {
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId: data.createOneRole.id,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords: objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isNonEmptyArray(fieldPermissionsToUpsert)) {
await upsertFieldPermissions({
variables: {
upsertFieldPermissionsInput: {
roleId: data.createOneRole.id,
fieldPermissions:
fieldPermissionsToUpsert.map((fieldPermission) => ({
objectMetadataId: fieldPermission.objectMetadataId,
fieldMetadataId: fieldPermission.fieldMetadataId,
canReadFieldValue: fieldPermission.canReadFieldValue,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
})) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.workspaceMembers)) {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
}
navigateSettings(SettingsPath.RoleDetail, {
roleId: data.createOneRole.id,
});
} else {
if (isDefined(dirtyFields.permissionFlags)) {
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId: roleId,
permissionFlagKeys:
settingsDraftRole.permissionFlags?.map(
(permissionFlag) => permissionFlag.flag,
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (ROLE_BASIC_KEYS.some((key) => key in dirtyFields)) {
await updateRole({
variables: {
updateRoleInput: {
id: roleId,
update: {
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canAccessAllTools: settingsDraftRole.canAccessAllTools,
canReadAllObjectRecords:
settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
},
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.objectPermissions)) {
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId: roleId,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords: objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isNonEmptyArray(fieldPermissionsToUpsert)) {
await upsertFieldPermissions({
variables: {
upsertFieldPermissionsInput: {
roleId: roleId,
fieldPermissions:
fieldPermissionsToUpsert.map((fieldPermission) => ({
objectMetadataId: fieldPermission.objectMetadataId,
fieldMetadataId: fieldPermission.fieldMetadataId,
canReadFieldValue: fieldPermission.canReadFieldValue,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
})) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
}
};
return {
saveDraftRoleToDB,
};
};
@@ -18,5 +18,6 @@ export const settingsDraftRoleFamilyState = createFamilyState<Role, string>({
workspaceMembers: [],
permissionFlags: [],
objectPermissions: [],
fieldPermissions: [],
},
});
@@ -1,4 +1,5 @@
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableHeaderText } from '@/ui/layout/table/components/TableHeaderText';
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
import { useRecoilState } from 'recoil';
@@ -57,7 +58,7 @@ export const SortableTableHeader = ({
)
) : null}
{Icon && <Icon size={14} />}
{label}
<TableHeaderText>{label}</TableHeaderText>
{isSortActive && align === 'left' ? (
isAsc ? (
<IconArrowUp size="14" />
@@ -4,6 +4,7 @@ const StyledTableHeader = styled.div<{
align?: 'left' | 'center' | 'right';
onClick?: () => void;
}>`
gap: ${({ theme }) => theme.spacing(1)};
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
@@ -0,0 +1,7 @@
import styled from '@emotion/styled';
const StyledTableHeaderText = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(0.5)};
`;
export { StyledTableHeaderText as TableHeaderText };
@@ -0,0 +1,112 @@
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { FieldPermission, ObjectPermission, Role } from '~/generated/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
export const MOCK_ROLE_ID_GRANTS_ALL = 'role-id-1';
export const MOCK_ROLE_ID_REVOKES_ALL = 'role-id-2';
export const MOCK_OBJECT_PERMISSION_1_REVOKES_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-1',
canReadObjectRecords: false,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
};
export const MOCK_OBJECT_PERMISSION_2_GRANTS_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-2',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
};
export const MOCK_OBJECT_PERMISSION_3_NULL_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-3',
canReadObjectRecords: null,
canUpdateObjectRecords: null,
canSoftDeleteObjectRecords: null,
canDestroyObjectRecords: null,
};
export const BASE_FIELD_PERMISSION: Omit<FieldPermission, 'roleId'> = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: MOCK_OBJECT_PERMISSION_1_REVOKES_ALL.objectMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
};
export const BASE_FIELD_PERMISSION_OBJECT_METADATA_3: Omit<
FieldPermission,
'roleId'
> = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: 'object-metadata-3',
canReadFieldValue: false,
canUpdateFieldValue: false,
};
export const BASE_ROLE_MOCK_GRANTS_ALL: Role = {
canAccessAllTools: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canUpdateAllSettings: true,
id: MOCK_ROLE_ID_GRANTS_ALL,
isEditable: true,
label: 'Role grants all',
workspaceMembers: [],
fieldPermissions: [
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_GRANTS_ALL },
{
...BASE_FIELD_PERMISSION_OBJECT_METADATA_3,
roleId: MOCK_ROLE_ID_GRANTS_ALL,
},
],
objectPermissions: [
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL,
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL,
MOCK_OBJECT_PERMISSION_3_NULL_ALL,
],
};
export const BASE_ROLE_MOCK_REVOKES_ALL: Role = {
canAccessAllTools: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: true,
id: MOCK_ROLE_ID_REVOKES_ALL,
isEditable: true,
label: 'Role revokes all',
workspaceMembers: [],
fieldPermissions: [
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_REVOKES_ALL },
{
...BASE_FIELD_PERMISSION_OBJECT_METADATA_3,
roleId: MOCK_ROLE_ID_REVOKES_ALL,
},
],
objectPermissions: [
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL,
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL,
MOCK_OBJECT_PERMISSION_3_NULL_ALL,
],
};
export const rolesMockHookWrapper = getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
settingsDraftRoleFamilyState(MOCK_ROLE_ID_GRANTS_ALL),
BASE_ROLE_MOCK_GRANTS_ALL,
);
snapshot.set(
settingsDraftRoleFamilyState(MOCK_ROLE_ID_REVOKES_ALL),
BASE_ROLE_MOCK_REVOKES_ALL,
);
},
});
@@ -0,0 +1,19 @@
import { turnOrderByIntoSort } from '../turnOrderByIntoSort';
describe('turnOrderByIntoSort', () => {
it('should be ok for "AscNullsFirst"', () => {
expect(turnOrderByIntoSort('AscNullsFirst')).toBe('asc');
});
it('should be ok for "AscNullsLast"', () => {
expect(turnOrderByIntoSort('AscNullsLast')).toBe('asc');
});
it('should be ok for "DescNullsFirst"', () => {
expect(turnOrderByIntoSort('DescNullsFirst')).toBe('desc');
});
it('should be ok for "DescNullsLast"', () => {
expect(turnOrderByIntoSort('DescNullsLast')).toBe('desc');
});
});
@@ -0,0 +1,12 @@
import { OrderBy } from '@/types/OrderBy';
import { assertUnreachable } from 'twenty-shared/utils';
export const turnOrderByIntoSort = (orderBy: OrderBy): 'asc' | 'desc' => {
if (orderBy === 'AscNullsFirst' || orderBy === 'AscNullsLast') {
return 'asc';
} else if (orderBy === 'DescNullsFirst' || orderBy === 'DescNullsLast') {
return 'desc';
} else {
assertUnreachable(orderBy);
}
};
@@ -451,106 +451,6 @@ describe('FieldPermissionService', () => {
),
);
});
it('should throw error when object is not readable (permission wise)', async () => {
const nonReadableObjectPermissions: ObjectRecordsPermissionsByRoleId = {
[testRoleId]: {
[testObjectMetadataId]: {
canRead: false,
canUpdate: false,
canSoftDelete: false,
canDestroy: false,
restrictedFields: {},
},
},
};
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
{
version: '1',
data: nonReadableObjectPermissions,
},
);
const input = createUpsertInput([
{
canUpdateFieldValue: false,
},
]);
await expect(
service.upsertFieldPermissions({
workspaceId: testWorkspaceId,
input,
}),
).rejects.toThrow(
new PermissionsException(
PermissionsExceptionMessage.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
),
);
});
it('should throw error when trying to restrict update on non-updatable object', async () => {
const nonUpdatableObjectPermissions: ObjectRecordsPermissionsByRoleId =
{
[testRoleId]: {
[testObjectMetadataId]: {
canRead: true,
canUpdate: false,
canSoftDelete: false,
canDestroy: false,
restrictedFields: {},
},
},
};
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
{
version: '1',
data: nonUpdatableObjectPermissions,
},
);
const input = createUpsertInput([
{
canUpdateFieldValue: false,
},
]);
await expect(
service.upsertFieldPermissions({
workspaceId: testWorkspaceId,
input,
}),
).rejects.toThrow(
new PermissionsException(
PermissionsExceptionMessage.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
),
);
});
it('should throw error when both canReadFieldValue and canUpdateFieldValue are null', async () => {
const input = createUpsertInput([
{
canReadFieldValue: null,
canUpdateFieldValue: null,
},
]);
await expect(
service.upsertFieldPermissions({
workspaceId: testWorkspaceId,
input,
}),
).rejects.toThrow(
new PermissionsException(
PermissionsExceptionMessage.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
),
);
});
});
describe('role validation errors', () => {
@@ -91,7 +91,23 @@ export class FieldPermissionService {
workspaceId,
}));
await this.fieldPermissionsRepository.upsert(fieldPermissions, {
const existingFieldPermissionsToDelete = existingFieldPermissions.filter(
(existingFieldPermissionToFilter) =>
fieldPermissionsToDeleteIds.includes(
existingFieldPermissionToFilter.id,
),
);
const fieldPermissionsToUpsert = fieldPermissions.filter(
(fieldPermissionToUpsert) =>
!existingFieldPermissionsToDelete.some(
(existingFieldPermissionToDelete) =>
existingFieldPermissionToDelete.fieldMetadataId ===
fieldPermissionToUpsert.fieldMetadataId,
),
);
await this.fieldPermissionsRepository.upsert(fieldPermissionsToUpsert, {
conflictPaths: ['fieldMetadataId', 'roleId'],
});
@@ -182,33 +198,6 @@ export class FieldPermissionService {
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
);
}
if (rolePermissionOnObject.canRead === false) {
throw new PermissionsException(
PermissionsExceptionMessage.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
);
}
if (
rolePermissionOnObject.canUpdate === false &&
fieldPermission.canUpdateFieldValue === false
) {
throw new PermissionsException(
PermissionsExceptionMessage.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
);
}
if (
fieldPermission.canUpdateFieldValue === null &&
fieldPermission.canReadFieldValue === null
) {
throw new PermissionsException(
PermissionsExceptionMessage.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
);
}
}
private async getRoleOrThrow({
@@ -4,6 +4,7 @@ import { Relation } from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
@@ -54,4 +55,7 @@ export class RoleDTO {
@Field(() => [ObjectPermissionDTO], { nullable: true })
objectPermissions?: ObjectPermissionDTO[];
@Field(() => [FieldPermissionDTO], { nullable: true })
fieldPermissions?: FieldPermissionDTO[];
}
@@ -40,7 +40,12 @@ export class RoleService {
where: {
workspaceId,
},
relations: ['roleTargets', 'permissionFlags', 'objectPermissions'],
relations: {
roleTargets: true,
permissionFlags: true,
objectPermissions: true,
fieldPermissions: true,
},
});
}
@@ -53,7 +58,12 @@ export class RoleService {
id,
workspaceId,
},
relations: ['roleTargets', 'permissionFlags'],
relations: {
roleTargets: true,
permissionFlags: true,
objectPermissions: true,
fieldPermissions: true,
},
});
}