object level override form (#11672)

This commit is contained in:
Weiko
2025-04-24 18:15:43 +02:00
committed by GitHub
parent 48e2581581
commit e55ecb4dcd
25 changed files with 708 additions and 196 deletions

View File

@ -517,6 +517,15 @@ export type CustomDomainValidRecords = {
records: Array<CustomDomainRecord>;
};
/** Database Event Action */
export enum DatabaseEventAction {
CREATED = 'CREATED',
DELETED = 'DELETED',
DESTROYED = 'DESTROYED',
RESTORED = 'RESTORED',
UPDATED = 'UPDATED'
}
export type DateFilter = {
eq?: InputMaybe<Scalars['Date']['input']>;
gt?: InputMaybe<Scalars['Date']['input']>;
@ -1004,7 +1013,7 @@ export type Mutation = {
uploadImage: Scalars['String']['output'];
uploadProfilePicture: Scalars['String']['output'];
uploadWorkspaceLogo: Scalars['String']['output'];
upsertOneObjectPermission: ObjectPermission;
upsertObjectPermissions: Array<ObjectPermission>;
upsertSettingPermissions: Array<SettingPermission>;
userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain;
@ -1368,8 +1377,8 @@ export type MutationUploadWorkspaceLogoArgs = {
};
export type MutationUpsertOneObjectPermissionArgs = {
upsertObjectPermissionInput: UpsertObjectPermissionInput;
export type MutationUpsertObjectPermissionsArgs = {
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
};
@ -1481,6 +1490,14 @@ export type ObjectPermission = {
roleId: Scalars['String']['output'];
};
export type ObjectPermissionInput = {
canDestroyObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canReadObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canSoftDeleteObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canUpdateObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
objectMetadataId: Scalars['String']['input'];
};
export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateFilter>;
@ -1500,6 +1517,21 @@ export type ObjectStandardOverrides = {
translations?: Maybe<Scalars['JSON']['output']>;
};
export type OnDbEventDto = {
__typename?: 'OnDbEventDTO';
action: DatabaseEventAction;
eventDate: Scalars['DateTime']['output'];
objectNameSingular: Scalars['String']['output'];
record: Scalars['JSON']['output'];
updatedFields?: Maybe<Array<Scalars['String']['output']>>;
};
export type OnDbEventInput = {
action?: InputMaybe<DatabaseEventAction>;
objectNameSingular?: InputMaybe<Scalars['String']['input']>;
recordId?: InputMaybe<Scalars['String']['input']>;
};
/** Onboarding status */
export enum OnboardingStatus {
COMPLETED = 'COMPLETED',
@ -2090,6 +2122,16 @@ export type SubmitFormStepInput = {
workflowRunId: Scalars['String']['input'];
};
export type Subscription = {
__typename?: 'Subscription';
onDbEvent: OnDbEventDto;
};
export type SubscriptionOnDbEventArgs = {
input: OnDbEventInput;
};
export enum SubscriptionInterval {
Day = 'Day',
Month = 'Month',
@ -2321,12 +2363,8 @@ export type UpdateWorkspaceInput = {
subdomain?: InputMaybe<Scalars['String']['input']>;
};
export type UpsertObjectPermissionInput = {
canDestroyObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canReadObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canSoftDeleteObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
canUpdateObjectRecords?: InputMaybe<Scalars['Boolean']['input']>;
objectMetadataId: Scalars['String']['input'];
export type UpsertObjectPermissionsInput = {
objectPermissions: Array<ObjectPermissionInput>;
roleId: Scalars['String']['input'];
};

View File

@ -929,7 +929,7 @@ export type Mutation = {
uploadImage: Scalars['String'];
uploadProfilePicture: Scalars['String'];
uploadWorkspaceLogo: Scalars['String'];
upsertOneObjectPermission: ObjectPermission;
upsertObjectPermissions: Array<ObjectPermission>;
upsertSettingPermissions: Array<SettingPermission>;
userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain;
@ -1243,8 +1243,8 @@ export type MutationUploadWorkspaceLogoArgs = {
};
export type MutationUpsertOneObjectPermissionArgs = {
upsertObjectPermissionInput: UpsertObjectPermissionInput;
export type MutationUpsertObjectPermissionsArgs = {
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
};
@ -1356,6 +1356,14 @@ export type ObjectPermission = {
roleId: Scalars['String'];
};
export type ObjectPermissionInput = {
canDestroyObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteObjectRecords?: InputMaybe<Scalars['Boolean']>;
canUpdateObjectRecords?: InputMaybe<Scalars['Boolean']>;
objectMetadataId: Scalars['String'];
};
export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateFilter>;
@ -2142,12 +2150,8 @@ export type UpdateWorkspaceInput = {
subdomain?: InputMaybe<Scalars['String']>;
};
export type UpsertObjectPermissionInput = {
canDestroyObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteObjectRecords?: InputMaybe<Scalars['Boolean']>;
canUpdateObjectRecords?: InputMaybe<Scalars['Boolean']>;
objectMetadataId: Scalars['String'];
export type UpsertObjectPermissionsInput = {
objectPermissions: Array<ObjectPermissionInput>;
roleId: Scalars['String'];
};
@ -2759,6 +2763,13 @@ export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{
export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } };
export type UpsertObjectPermissionsMutationVariables = Exact<{
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
}>;
export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> };
export type UpsertSettingPermissionsMutationVariables = Exact<{
upsertSettingPermissionsInput: UpsertSettingPermissionsInput;
}>;
@ -5058,6 +5069,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 UpsertObjectPermissionsDocument = gql`
mutation UpsertObjectPermissions($upsertObjectPermissionsInput: UpsertObjectPermissionsInput!) {
upsertObjectPermissions(
upsertObjectPermissionsInput: $upsertObjectPermissionsInput
) {
...ObjectPermissionFragment
}
}
${ObjectPermissionFragmentFragmentDoc}`;
export type UpsertObjectPermissionsMutationFn = Apollo.MutationFunction<UpsertObjectPermissionsMutation, UpsertObjectPermissionsMutationVariables>;
/**
* __useUpsertObjectPermissionsMutation__
*
* To run a mutation, you first call `useUpsertObjectPermissionsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpsertObjectPermissionsMutation` 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 [upsertObjectPermissionsMutation, { data, loading, error }] = useUpsertObjectPermissionsMutation({
* variables: {
* upsertObjectPermissionsInput: // value for 'upsertObjectPermissionsInput'
* },
* });
*/
export function useUpsertObjectPermissionsMutation(baseOptions?: Apollo.MutationHookOptions<UpsertObjectPermissionsMutation, UpsertObjectPermissionsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpsertObjectPermissionsMutation, UpsertObjectPermissionsMutationVariables>(UpsertObjectPermissionsDocument, options);
}
export type UpsertObjectPermissionsMutationHookResult = ReturnType<typeof useUpsertObjectPermissionsMutation>;
export type UpsertObjectPermissionsMutationResult = Apollo.MutationResult<UpsertObjectPermissionsMutation>;
export type UpsertObjectPermissionsMutationOptions = Apollo.BaseMutationOptions<UpsertObjectPermissionsMutation, UpsertObjectPermissionsMutationVariables>;
export const UpsertSettingPermissionsDocument = gql`
mutation UpsertSettingPermissions($upsertSettingPermissionsInput: UpsertSettingPermissionsInput!) {
upsertSettingPermissions(

View File

@ -2,10 +2,12 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { settingsRoleIdsState } from '@/settings/roles/states/settingsRoleIdsState';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useEffect } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Role, useGetRolesQuery } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const SettingsRolesQueryEffect = () => {
const { data, loading } = useGetRolesQuery({
@ -17,11 +19,18 @@ export const SettingsRolesQueryEffect = () => {
);
const populateRoles = useRecoilCallback(
({ set }) =>
({ set, snapshot }) =>
(roles: Role[]) => {
const roleIds = roles.map((role) => role.id);
set(settingsRoleIdsState, roleIds);
roles.forEach((role) => {
const persistedRole = getSnapshotValue(
snapshot,
settingsPersistedRoleFamilyState(role.id),
);
if (isDeeplyEqual(role, persistedRole)) {
return;
}
set(settingsDraftRoleFamilyState(role.id), role);
set(settingsPersistedRoleFamilyState(role.id), role);
});

View File

@ -0,0 +1,15 @@
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { gql } from '@apollo/client';
export const UPSERT_OBJECT_PERMISSIONS = gql`
${OBJECT_PERMISSION_FRAGMENT}
mutation UpsertObjectPermissions(
$upsertObjectPermissionsInput: UpsertObjectPermissionsInput!
) {
upsertObjectPermissions(
upsertObjectPermissionsInput: $upsertObjectPermissionsInput
) {
...ObjectPermissionFragment
}
}
`;

View File

@ -14,11 +14,13 @@ const StyledRolePermissionsContainer = styled.div`
type SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
isCreateMode: boolean;
};
export const SettingsRolePermissions = ({
roleId,
isEditable,
isCreateMode,
}: SettingsRolePermissionsProps) => {
const isPermissionsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
@ -30,7 +32,7 @@ export const SettingsRolePermissions = ({
roleId={roleId}
isEditable={isEditable}
/>
{isPermissionsV2Enabled && (
{isPermissionsV2Enabled && !isCreateMode && (
<SettingsRolePermissionsObjectLevelSection
roleId={roleId}
isEditable={isEditable}

View File

@ -3,10 +3,10 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
const SettingsRolePermissionsWrapper = (
args: React.ComponentProps<typeof SettingsRolePermissions>,
@ -25,6 +25,7 @@ const SettingsRolePermissionsWrapper = (
<SettingsRolePermissions
roleId={args.roleId}
isEditable={args.isEditable}
isCreateMode={args.isCreateMode}
/>
);
};
@ -42,6 +43,7 @@ export const Default: Story = {
args: {
roleId: '1',
isEditable: true,
isCreateMode: false,
},
};
@ -49,11 +51,14 @@ export const ReadOnly: Story = {
args: {
roleId: '1',
isEditable: false,
isCreateMode: false,
},
};
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
isEditable: true,
isCreateMode: true,
},
};

View File

@ -19,7 +19,8 @@ export const SettingsRolePermissionsObjectLevelObjectPickerDropdownContent = ({
}: SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps) => {
const [searchFilter, setSearchFilter] = useState('');
const { objectMetadataItems } = useFilteredObjectMetadataItems();
const { alphaSortedActiveObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const { getIcon } = useIcons();

View File

@ -1,3 +1,4 @@
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useTheme } from '@emotion/react';
@ -46,12 +47,8 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
settingsDraftRoleFamilyState(objectPermission.roleId),
);
const permissionMappings = {
canReadObjectRecords: 'canReadAllObjectRecords',
canUpdateObjectRecords: 'canUpdateAllObjectRecords',
canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords',
canDestroyObjectRecords: 'canDestroyAllObjectRecords',
} as const;
const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
type ObjectPermissionKey = keyof typeof permissionMappings;

View File

@ -1,24 +1,30 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelObjectPickerDropdownContent } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent';
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 { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
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 { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { v4 } from 'uuid';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
// const StyledCreateObjectOverrideSection = styled(Section)`
// border-top: 1px solid ${({ theme }) => theme.border.color.light};
// display: flex;
// justify-content: flex-end;
// padding-top: ${({ theme }) => theme.spacing(2)};
// padding-bottom: ${({ theme }) => theme.spacing(2)};
// `;
const StyledCreateObjectOverrideSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
@ -36,11 +42,14 @@ const StyledNoOverride = styled(TableCell)`
export const SettingsRolePermissionsObjectLevelSection = ({
roleId,
isEditable,
}: SettingsRolePermissionsObjectLevelSectionProps) => {
const settingsDraftRole = useRecoilValue(
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const navigate = useNavigateSettings();
const objectMetadataItems = useObjectMetadataItems();
const objectMetadataMap = objectMetadataItems.objectMetadataItems.reduce(
@ -51,29 +60,52 @@ export const SettingsRolePermissionsObjectLevelSection = ({
{} as Record<string, ObjectMetadataItem>,
);
const objectPermissions = settingsDraftRole.objectPermissions;
const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter(
(objectPermission) =>
(isDefined(objectPermission.canReadObjectRecords) &&
objectPermission.canReadObjectRecords !==
settingsDraftRole.canReadAllObjectRecords) ||
(isDefined(objectPermission.canUpdateObjectRecords) &&
objectPermission.canUpdateObjectRecords !==
settingsDraftRole.canUpdateAllObjectRecords) ||
(isDefined(objectPermission.canSoftDeleteObjectRecords) &&
objectPermission.canSoftDeleteObjectRecords !==
settingsDraftRole.canSoftDeleteAllObjectRecords) ||
(isDefined(objectPermission.canDestroyObjectRecords) &&
objectPermission.canDestroyObjectRecords !==
settingsDraftRole.canDestroyAllObjectRecords),
);
// const handleSelectObjectMetadata = (objectMetadataId: string) => {
// setSettingsDraftRole((draftRole) => ({
// ...draftRole,
// objectPermissions: [
// ...(draftRole.objectPermissions ?? []),
// { objectMetadataId, roleId, id: v4() },
// ],
// }));
// };
const handleSelectObjectMetadata = (objectMetadataId: string) => {
setSettingsDraftRole((draftRole) => ({
...draftRole,
objectPermissions: [
...(draftRole.objectPermissions ?? []).filter(
(permission) =>
permission.objectMetadataId !== objectMetadataId ||
permission.roleId !== roleId,
),
{ objectMetadataId, roleId, id: v4() },
],
}));
navigate(SettingsPath.RoleObjectLevel, {
roleId,
objectMetadataId,
});
};
return (
<Section>
<H2Title
title={t`Object-Level Permissions`}
description={t`Set additional object-level permissions`}
description={t`Ability to interact with specific objects`}
/>
<Table>
<SettingsRolePermissionsObjectLevelTableHeader />
<StyledTableRows>
{isDefined(objectPermissions) && objectPermissions?.length > 0 ? (
objectPermissions?.map((objectPermission) => (
{isDefined(filteredObjectPermissions) &&
filteredObjectPermissions?.length > 0 ? (
filteredObjectPermissions?.map((objectPermission) => (
<SettingsRolePermissionsObjectLevelTableRow
key={objectPermission.id}
objectPermission={objectPermission}
@ -87,14 +119,14 @@ export const SettingsRolePermissionsObjectLevelSection = ({
)}
</StyledTableRows>
</Table>
{/* <StyledCreateObjectOverrideSection>
<StyledCreateObjectOverrideSection>
<Dropdown
dropdownId="role-object-select"
dropdownHotkeyScope={{ scope: 'roleObject' }}
clickableComponent={
<Button
Icon={IconPlus}
title={t`Add Object`}
title={t`Add rule`}
variant="secondary"
size="small"
disabled={!isEditable}
@ -102,12 +134,16 @@ export const SettingsRolePermissionsObjectLevelSection = ({
}
dropdownComponents={
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
excludedObjectMetadataIds={[]}
excludedObjectMetadataIds={
filteredObjectPermissions?.map(
(objectPermission) => objectPermission.objectMetadataId,
) ?? []
}
onSelect={handleSelectObjectMetadata}
/>
}
/>
</StyledCreateObjectOverrideSection> */}
</StyledCreateObjectOverrideSection>
</Section>
);
};

View File

@ -3,9 +3,9 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
export const SettingsRolePermissionsObjectLevelTableHeader = () => (
<TableRow>
<TableRow gridAutoColumns="180px 1fr 1fr">
<TableHeader>{t`Object`}</TableHeader>
<TableHeader>{t`Permission overrides`}</TableHeader>
<TableHeader>{t`Permissions`}</TableHeader>
<TableHeader></TableHeader>
</TableRow>
);

View File

@ -5,7 +5,11 @@ 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 { IconChevronRight, useIcons } from 'twenty-ui/display';
import {
IconChevronRight,
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { ObjectPermission } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
@ -44,6 +48,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
roleId: objectPermission.roleId,
objectMetadataId: objectPermission.objectMetadataId,
})}
gridAutoColumns="180px 1fr 1fr"
>
<StyledNameTableCell>
{!!Icon && (
@ -54,7 +59,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
/>
)}
<StyledNameLabel title={objectMetadataItem.labelPlural}>
{objectMetadataItem.labelPlural}
<OverflowingTextWithTooltip text={objectMetadataItem.labelPlural} />
</StyledNameLabel>
</StyledNameTableCell>
<TableCell>

View File

@ -0,0 +1,95 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconReload, IconX } from 'twenty-ui/display';
import { Checkbox } from 'twenty-ui/input';
export type OverridableCheckboxType = 'default' | 'override' | 'no_cta';
const StyledOverridableCheckboxContainer = styled.div`
align-items: center;
display: inline-flex;
justify-content: flex-start;
width: 48px;
`;
const StyledOverridableCheckboxContainerItem = styled.div`
align-items: center;
display: flex;
height: 24px;
justify-content: center;
width: 24px;
`;
const StyledIconWrapper = styled.div<{
isDisabled?: boolean;
}>`
align-items: center;
cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')};
display: flex;
height: 100%;
justify-content: center;
opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
width: 100%;
`;
export type OverridableCheckboxProps = {
type?: OverridableCheckboxType;
onChange: () => void;
checked: boolean;
disabled: boolean;
};
export const OverridableCheckbox = ({
type = 'default',
onChange,
checked,
disabled,
}: OverridableCheckboxProps) => {
const theme = useTheme();
return (
<StyledOverridableCheckboxContainer>
{type === 'default' && (
<>
<StyledOverridableCheckboxContainerItem>
<Checkbox checked={true} disabled={true} />
</StyledOverridableCheckboxContainerItem>
<StyledOverridableCheckboxContainerItem>
<StyledIconWrapper
onClick={disabled ? undefined : onChange}
isDisabled={disabled}
>
<IconX
size={theme.icon.size.md}
color={theme.font.color.secondary}
/>
</StyledIconWrapper>
</StyledOverridableCheckboxContainerItem>
</>
)}
{type === 'override' && (
<>
<StyledOverridableCheckboxContainerItem>
<Checkbox checked={false} disabled={true} />
</StyledOverridableCheckboxContainerItem>
<StyledOverridableCheckboxContainerItem>
<StyledIconWrapper
onClick={disabled ? undefined : onChange}
isDisabled={disabled}
>
<IconReload
size={theme.icon.size.md}
color={theme.adaptiveColors.orange4}
/>
</StyledIconWrapper>
</StyledOverridableCheckboxContainerItem>
</>
)}
{type === 'no_cta' && (
<StyledOverridableCheckboxContainerItem>
<Checkbox checked={checked} disabled={disabled} onChange={onChange} />
</StyledOverridableCheckboxContainerItem>
)}
</StyledOverridableCheckboxContainer>
);
};

View File

@ -1,13 +1,14 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader';
import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableRow';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
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 { 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 { useRecoilValue } from 'recoil';
import { useRecoilState } from 'recoil';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { ObjectPermission } from '~/generated-metadata/graphql';
const StyledTable = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
@ -27,7 +28,7 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
roleId,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps) => {
const settingsDraftRole = useRecoilValue(
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
@ -42,40 +43,56 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
const objectLabel = objectMetadataItem.labelPlural;
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
{
key: 'canReadObjectRecords',
label: t`See Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canReadObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
const updateObjectPermission = (
permissionKey: keyof ObjectPermission,
value: boolean | null,
) => {
setSettingsDraftRole((currentRole) => {
const updatedPermissions = currentRole.objectPermissions?.map((perm) => {
if (perm.objectMetadataId === objectMetadataItem.id) {
return { ...perm, [permissionKey]: value };
}
return perm;
});
return { ...currentRole, objectPermissions: updatedPermissions };
});
};
const objectPermissionsConfig: SettingsRolePermissionsObjectLevelPermission[] =
[
{
key: 'canReadObjectRecords',
label: t`See Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canReadObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canReadObjectRecords', value);
},
},
},
{
key: 'canUpdateObjectRecords',
label: t`Edit Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
{
key: 'canUpdateObjectRecords',
label: t`Edit Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canUpdateObjectRecords', value);
},
},
},
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canSoftDeleteObjectRecords', value);
},
},
},
{
key: 'canDestroyObjectRecords',
label: t`Destroy Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
{
key: 'canDestroyObjectRecords',
label: t`Destroy Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords,
setValue: (value: boolean | null) => {
updateObjectPermission('canDestroyObjectRecords', value);
},
},
},
];
];
return (
<Section>
@ -84,13 +101,17 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
description={t`Ability to interact with this specific object`}
/>
<StyledTable>
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader />
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader />
<StyledTableRows>
{objectPermissionsConfig.map((permission) => (
<SettingsRolePermissionsObjectsTableRow
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow
key={permission.key}
permission={permission}
isEditable={settingsDraftRole.isEditable}
settingsDraftRoleObjectPermissions={
settingsDraftRoleObjectPermissions
}
roleId={roleId}
/>
))}
</StyledTableRows>

View File

@ -2,9 +2,9 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader =
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader =
() => (
<TableRow gridAutoColumns="1fr 24px">
<TableRow gridAutoColumns="1fr 48px">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader aria-label={t`Actions`}></TableHeader>
</TableRow>

View File

@ -0,0 +1,141 @@
import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox';
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } 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 { 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 { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
import type { Role } from '~/generated/graphql';
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledOverrideInfo = styled.span`
background: ${({ theme }) => theme.adaptiveColors.orange1};
border-radius: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.color.orange};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledPermissionCell = styled(TableCell)`
align-items: center;
display: flex;
flex: 1;
gap: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledCheckboxCell = styled(TableCell)`
align-items: center;
display: flex;
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledTableRow = styled(TableRow)`
align-items: center;
display: flex;
`;
type OverridableCheckboxType = 'no_cta' | 'default' | 'override';
type SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps = {
permission: SettingsRolePermissionsObjectLevelPermission;
isEditable: boolean;
settingsDraftRoleObjectPermissions: ObjectPermission;
roleId: string;
};
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
({
permission,
isEditable,
settingsDraftRoleObjectPermissions,
roleId,
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps) => {
const theme = useTheme();
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const label = permission.label;
const { Icon } =
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
const settingsDraftRoleObjectPermissionValue =
settingsDraftRoleObjectPermissions[
permission.key as keyof ObjectPermission
];
const rolePermission =
permissionMappings[permission.key as keyof typeof permissionMappings];
const settingsDraftRoleGlobalPermissionValue =
settingsDraftRole[rolePermission as keyof Role];
const isChecked = !!settingsDraftRoleObjectPermissionValue;
const isRevoked =
isDefined(settingsDraftRoleObjectPermissionValue) &&
settingsDraftRoleGlobalPermissionValue === true &&
isChecked === false;
let checkboxType: OverridableCheckboxType;
if (
settingsDraftRoleGlobalPermissionValue === true &&
settingsDraftRoleObjectPermissionValue === false
) {
checkboxType = 'override';
} else if (settingsDraftRoleGlobalPermissionValue === false) {
checkboxType = 'no_cta';
} else {
checkboxType = 'default';
}
const handleCheckboxChange = () => {
if (!isEditable) return;
if (checkboxType === 'default') {
permission.setValue(false);
} else if (checkboxType === 'override') {
permission.setValue(null);
} else if (checkboxType === 'no_cta') {
permission.setValue(!isChecked);
}
};
return (
<StyledTableRow>
<StyledPermissionCell>
<Icon size={theme.icon.size.sm} />
<StyledLabel>{label}</StyledLabel>
{isRevoked ? (
<StyledOverrideInfo>
{t`Revoked for this object`}
</StyledOverrideInfo>
) : null}
</StyledPermissionCell>
<StyledCheckboxCell>
<OverridableCheckbox
onChange={handleCheckboxChange}
disabled={!isEditable}
type={checkboxType}
checked={isChecked}
/>
</StyledCheckboxCell>
</StyledTableRow>
);
};

View File

@ -5,7 +5,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
@ -37,12 +36,11 @@ export const SettingsRolePermissionsObjectsSection = ({
{
key: 'canReadObjectRecords',
label: t`See Records on All Objects`,
overriddenBy:
revokedBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canReadObjectRecords) &&
permission.canReadObjectRecords !==
settingsDraftRole.canReadAllObjectRecords,
permission.canReadObjectRecords === false &&
settingsDraftRole.canReadAllObjectRecords === true,
)?.length ?? 0,
value: settingsDraftRole.canReadAllObjectRecords,
setValue: (value: boolean) => {
@ -55,12 +53,11 @@ export const SettingsRolePermissionsObjectsSection = ({
{
key: 'canUpdateObjectRecords',
label: t`Edit Records on All Objects`,
overriddenBy:
revokedBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canUpdateObjectRecords) &&
permission.canUpdateObjectRecords !==
settingsDraftRole.canUpdateAllObjectRecords,
permission.canUpdateObjectRecords === false &&
settingsDraftRole.canUpdateAllObjectRecords === true,
)?.length ?? 0,
value: settingsDraftRole.canUpdateAllObjectRecords,
setValue: (value: boolean) => {
@ -73,12 +70,11 @@ export const SettingsRolePermissionsObjectsSection = ({
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on All Objects`,
overriddenBy:
revokedBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canSoftDeleteObjectRecords) &&
permission.canSoftDeleteObjectRecords !==
settingsDraftRole.canSoftDeleteAllObjectRecords,
permission.canSoftDeleteObjectRecords === false &&
settingsDraftRole.canSoftDeleteAllObjectRecords === true,
)?.length ?? 0,
value: settingsDraftRole.canSoftDeleteAllObjectRecords,
setValue: (value: boolean) => {
@ -91,12 +87,11 @@ export const SettingsRolePermissionsObjectsSection = ({
{
key: 'canDestroyObjectRecords',
label: t`Destroy Records on All Objects`,
overriddenBy:
revokedBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canDestroyObjectRecords) &&
permission.canDestroyObjectRecords !==
settingsDraftRole.canDestroyAllObjectRecords,
permission.canDestroyObjectRecords === false &&
settingsDraftRole.canDestroyAllObjectRecords === true,
)?.length ?? 0,
value: settingsDraftRole.canDestroyAllObjectRecords,
setValue: (value: boolean) => {
@ -112,7 +107,7 @@ export const SettingsRolePermissionsObjectsSection = ({
<Section>
<H2Title
title={t`Objects`}
description={t`Ability to interact with each object`}
description={t`Actions you can perform on all objects`}
/>
<StyledTable>
<SettingsRolePermissionsObjectsTableHeader

View File

@ -50,10 +50,10 @@ export const SettingsRolePermissionsObjectsTableRow = ({
}: SettingsRolePermissionsObjectsTableRowProps) => {
const theme = useTheme();
const isOverriddenBy = permission.overriddenBy;
const isOverridden = isOverriddenBy && isOverriddenBy > 0;
const revokedBy = permission.revokedBy;
const isRevoked = revokedBy && revokedBy > 0;
const label = permission.label;
const pluralizedObject = pluralize('object', isOverriddenBy);
const pluralizedObject = pluralize('object', revokedBy);
const { Icon } = SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
@ -62,9 +62,9 @@ export const SettingsRolePermissionsObjectsTableRow = ({
<StyledPermissionCell>
<Icon size={theme.icon.size.sm} />
<StyledLabel>{label}</StyledLabel>
{isOverridden ? (
{isRevoked ? (
<StyledOverrideInfo>
{t`Overridden on ${isOverriddenBy} ${pluralizedObject}`}
{t`Revoked on ${revokedBy} ${pluralizedObject}`}
</StyledOverrideInfo>
) : null}
</StyledPermissionCell>
@ -73,7 +73,7 @@ export const SettingsRolePermissionsObjectsTableRow = ({
checked={permission.value ?? false}
onChange={() => permission.setValue(!permission.value)}
disabled={!isEditable}
accent={isOverridden ? CheckboxAccent.Orange : CheckboxAccent.Blue}
accent={isRevoked ? CheckboxAccent.Orange : CheckboxAccent.Blue}
/>
</StyledCheckboxCell>
</StyledTableRow>

View File

@ -0,0 +1,7 @@
export const SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING =
{
canReadObjectRecords: 'canReadAllObjectRecords',
canUpdateObjectRecords: 'canUpdateAllObjectRecords',
canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords',
canDestroyObjectRecords: 'canDestroyAllObjectRecords',
} as const;

View File

@ -2,7 +2,14 @@ import { ReactNode } from 'react';
export type SettingsRolePermissionsObjectPermission = {
key: string;
label: string | ReactNode;
value?: boolean | null;
value?: boolean;
setValue: (value: boolean) => void;
overriddenBy?: number;
revokedBy?: number;
};
export type SettingsRolePermissionsObjectLevelPermission = {
key: string;
label: string | ReactNode;
value?: boolean | null;
setValue: (value: boolean | null) => void;
};

View File

@ -29,6 +29,7 @@ import {
Role,
useCreateOneRoleMutation,
useUpdateOneRoleMutation,
useUpsertObjectPermissionsMutation,
useUpsertSettingPermissionsMutation,
} from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@ -67,6 +68,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const [createRole] = useCreateOneRoleMutation();
const [updateRole] = useUpdateOneRoleMutation();
const [upsertSettingPermissions] = useUpsertSettingPermissionsMutation();
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
@ -142,25 +144,54 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
},
},
onCompleted: async (data) => {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
if (isDefined(dirtyFields.workspaceMembers)) {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
}
await upsertSettingPermissions({
variables: {
upsertSettingPermissionsInput: {
roleId: data.createOneRole.id,
settingPermissionKeys:
settingsDraftRole.settingPermissions?.map(
(settingPermission) => settingPermission.setting,
) ?? [],
if (isDefined(dirtyFields.settingPermissions)) {
await upsertSettingPermissions({
variables: {
upsertSettingPermissionsInput: {
roleId: data.createOneRole.id,
settingPermissionKeys:
settingsDraftRole.settingPermissions?.map(
(settingPermission) => settingPermission.setting,
) ?? [],
},
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
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) ?? ''],
});
}
navigateSettings(SettingsPath.RoleDetail, {
roleId: data.createOneRole.id,
@ -206,6 +237,30 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
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) ?? ''],
});
}
}
};
@ -251,6 +306,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
<SettingsRolePermissions
roleId={roleId}
isEditable={isRoleEditable}
isCreateMode={isCreateMode}
/>
)}
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS && (

View File

@ -2,10 +2,13 @@ import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/Setti
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Role } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type SettingsRoleEditEffectProps = {
roleId: string;
@ -17,24 +20,35 @@ export const SettingsRoleEditEffect = ({
const [isInitialized, setIsInitialized] = useState(false);
const role = useRecoilValue(settingsPersistedRoleFamilyState(roleId));
const setDraftRole = useSetRecoilState(settingsDraftRoleFamilyState(roleId));
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID,
);
const updateDraftRoleIfNeeded = useRecoilCallback(
({ set, snapshot }) =>
(newRole: Role) => {
const currentPersistedRole = getSnapshotValue(
snapshot,
settingsPersistedRoleFamilyState(newRole.id),
);
if (!isDeeplyEqual(newRole, currentPersistedRole)) {
set(settingsDraftRoleFamilyState(newRole.id), newRole);
}
},
[],
);
useEffect(() => {
if (isInitialized) {
if (isInitialized || !isDefined(role)) {
return;
}
setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT);
if (isDefined(role)) {
setDraftRole(role);
setIsInitialized(true);
}
}, [isInitialized, role, setActiveTabId, setDraftRole]);
updateDraftRoleIfNeeded(role);
setIsInitialized(true);
}, [isInitialized, role, setActiveTabId, updateDraftRoleIfNeeded]);
return <></>;
};

View File

@ -1,14 +1,30 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsNotEmpty,
IsOptional,
IsUUID,
} from 'class-validator';
@InputType()
export class UpsertObjectPermissionInput {
export class UpsertObjectPermissionsInput {
@IsUUID()
@IsNotEmpty()
@Field()
roleId: string;
@IsArray()
@ArrayMinSize(1)
@IsNotEmpty()
@Field(() => [ObjectPermissionInput])
objectPermissions: ObjectPermissionInput[];
}
@InputType()
export class ObjectPermissionInput {
@IsUUID()
@IsNotEmpty()
@Field()

View File

@ -1,10 +1,10 @@
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input';
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import {
PermissionsException,
@ -25,24 +25,29 @@ export class ObjectPermissionService {
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
public async upsertObjectPermission({
public async upsertObjectPermissions({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertObjectPermissionInput;
}): Promise<ObjectPermissionEntity | null> {
input: UpsertObjectPermissionsInput;
}): Promise<ObjectPermissionEntity[]> {
try {
await this.validateRoleIsEditableOrThrow({
roleId: input.roleId,
workspaceId,
});
const result = await this.objectPermissionRepository.upsert(
{
const objectPermissions = input.objectPermissions.map(
(objectPermission) => ({
...objectPermission,
roleId: input.roleId,
workspaceId,
...input,
},
}),
);
const result = await this.objectPermissionRepository.upsert(
objectPermissions,
{
conflictPaths: ['objectMetadataId', 'roleId'],
},
@ -61,9 +66,14 @@ export class ObjectPermissionService {
},
);
return this.objectPermissionRepository.findOne({
return this.objectPermissionRepository.find({
where: {
id: objectPermissionId,
roleId: input.roleId,
objectMetadataId: In(
input.objectPermissions.map(
(objectPermission) => objectPermission.objectMetadataId,
),
),
},
});
} catch (error) {
@ -71,7 +81,9 @@ export class ObjectPermissionService {
error,
roleId: input.roleId,
workspaceId,
objectMetadataId: input.objectMetadataId,
objectMetadataIds: input.objectPermissions.map(
(objectPermission) => objectPermission.objectMetadataId,
),
});
throw error;
@ -82,12 +94,12 @@ export class ObjectPermissionService {
error,
roleId,
workspaceId,
objectMetadataId,
objectMetadataIds,
}: {
error: Error;
roleId: string;
workspaceId: string;
objectMetadataId: string;
objectMetadataIds: string[];
}) {
if (error.message.includes('violates foreign key constraint')) {
const role = await this.roleRepository.findOne({
@ -104,14 +116,14 @@ export class ObjectPermissionService {
);
}
const objectMetadata = await this.objectMetadataRepository.findOne({
const objectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
id: objectMetadataId,
id: In(objectMetadataIds),
},
});
if (!isDefined(objectMetadata)) {
if (objectMetadata.length !== objectMetadataIds.length) {
throw new PermissionsException(
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,

View File

@ -17,7 +17,7 @@ import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input';
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import {
@ -147,21 +147,18 @@ export class RoleResolver {
return deletedRoleId;
}
@Mutation(() => ObjectPermissionDTO)
async upsertOneObjectPermission(
@Mutation(() => [ObjectPermissionDTO])
async upsertObjectPermissions(
@AuthWorkspace() workspace: Workspace,
@Args('upsertObjectPermissionInput')
upsertObjectPermissionInput: UpsertObjectPermissionInput,
) {
@Args('upsertObjectPermissionsInput')
upsertObjectPermissionsInput: UpsertObjectPermissionsInput,
): Promise<ObjectPermissionDTO[]> {
await this.validatePermissionsV2EnabledOrThrow(workspace);
const objectPermission =
await this.objectPermissionService.upsertObjectPermission({
workspaceId: workspace.id,
input: upsertObjectPermissionInput,
});
return objectPermission;
return this.objectPermissionService.upsertObjectPermissions({
workspaceId: workspace.id,
input: upsertObjectPermissionsInput,
});
}
@Mutation(() => [SettingPermissionDTO])
@ -169,7 +166,7 @@ export class RoleResolver {
@AuthWorkspace() workspace: Workspace,
@Args('upsertSettingPermissionsInput')
upsertSettingPermissionsInput: UpsertSettingPermissionsInput,
) {
): Promise<SettingPermissionDTO[]> {
await this.validatePermissionsV2EnabledOrThrow(workspace);
return this.settingPermissionService.upsertSettingPermissions({

View File

@ -72,19 +72,12 @@ describe('roles permissions', () => {
});
afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsEnabled',
false,
);
const disablePermissionsV2Query = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery);
await makeGraphqlAPIRequest(disablePermissionsV2Query);
});
@ -480,9 +473,10 @@ describe('roles permissions', () => {
roleId: string;
}) => `
mutation UpsertObjectPermissions {
upsertOneObjectPermission(upsertObjectPermissionInput: {objectMetadataId: "${objectMetadataId}", roleId: "${roleId}", canUpdateObjectRecords: true}) {
upsertObjectPermissions(upsertObjectPermissionsInput: { roleId: "${roleId}", objectPermissions: [{objectMetadataId: "${objectMetadataId}", canUpdateObjectRecords: true}]}) {
id
roleId
objectMetadataId
canUpdateObjectRecords
}
}
@ -540,12 +534,15 @@ describe('roles permissions', () => {
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
expect(res.body.data.upsertOneObjectPermission.roleId).toBe(
createdEditableRoleId,
expect(res.body.data.upsertObjectPermissions).toEqual(
expect.arrayContaining([
expect.objectContaining({
roleId: createdEditableRoleId,
objectMetadataId: listingObjectId,
canUpdateObjectRecords: true,
}),
]),
);
expect(
res.body.data.upsertOneObjectPermission.canUpdateObjectRecords,
).toBe(true);
});
});
});