Add object level form to role creation (#12826)

## Context
- Add object-level form to role creation
- Add isSaving props for save button isLoading state
<img width="594" alt="Screenshot 2025-06-24 at 15 03 59"
src="https://github.com/user-attachments/assets/77d9d399-4e1a-4e35-be45-c19100ef06c1"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-06-24 15:15:37 +02:00
committed by GitHub
parent 540f3ffd67
commit 8cf7649a4c
9 changed files with 190 additions and 162 deletions

View File

@ -3,10 +3,9 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
const SettingsRoleAssignmentWrapper = (
args: React.ComponentProps<typeof SettingsRoleAssignment>,
@ -41,6 +40,6 @@ export const Default: Story = {
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
roleId: 'newRoleId',
},
};

View File

@ -12,13 +12,11 @@ const StyledRolePermissionsContainer = styled.div`
type SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
isCreateMode: boolean;
};
export const SettingsRolePermissions = ({
roleId,
isEditable,
isCreateMode,
}: SettingsRolePermissionsProps) => {
return (
<StyledRolePermissionsContainer>
@ -26,12 +24,10 @@ export const SettingsRolePermissions = ({
roleId={roleId}
isEditable={isEditable}
/>
{!isCreateMode && (
<SettingsRolePermissionsObjectLevelSection
roleId={roleId}
isEditable={isEditable}
/>
)}
<SettingsRolePermissionsObjectLevelSection
roleId={roleId}
isEditable={isEditable}
/>
<SettingsRolePermissionsSettingsSection
roleId={roleId}
isEditable={isEditable}

View File

@ -4,7 +4,6 @@ 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';
@ -57,7 +56,7 @@ export const ReadOnly: Story = {
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
roleId: 'newRoleId',
isEditable: true,
isCreateMode: true,
},

View File

@ -4,7 +4,6 @@ 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';
@ -57,7 +56,7 @@ export const ReadOnly: Story = {
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
roleId: 'newRoleId',
isEditable: true,
isCreateMode: false,
},

View File

@ -20,10 +20,10 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
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 { v4 } from 'uuid';
import {
Role,
useCreateOneRoleMutation,
@ -65,6 +65,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const [upsertSettingPermissions] = useUpsertSettingPermissionsMutation();
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
const [isSaving, setIsSaving] = useState(false);
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
@ -114,6 +116,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
};
const handleSave = async () => {
setIsSaving(true);
const dirtyFields = getDirtyFields(
settingsDraftRole,
settingsPersistedRole,
@ -126,151 +130,156 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
return;
}
if (isCreateMode) {
const roleId = v4();
const { data } = await createRole({
variables: {
createRoleInput: {
id: roleId,
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
if (!data) {
return;
}
if (isDefined(dirtyFields.workspaceMembers)) {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
}
if (isDefined(dirtyFields.settingPermissions)) {
await upsertSettingPermissions({
try {
if (isCreateMode) {
const { data } = await createRole({
variables: {
upsertSettingPermissionsInput: {
roleId: data.createOneRole.id,
settingPermissionKeys:
settingsDraftRole.settingPermissions?.map(
(settingPermission) => settingPermission.setting,
) ?? [],
},
},
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,
});
} else {
if (isDefined(dirtyFields.settingPermissions)) {
await upsertSettingPermissions({
variables: {
upsertSettingPermissionsInput: {
roleId: roleId,
settingPermissionKeys:
settingsDraftRole.settingPermissions?.map(
(settingPermission) => settingPermission.setting,
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (ROLE_BASIC_KEYS.some((key) => key in dirtyFields)) {
await updateRole({
variables: {
updateRoleInput: {
createRoleInput: {
id: roleId,
update: {
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canReadAllObjectRecords:
settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
label: settingsDraftRole.label,
description: settingsDraftRole.description,
icon: settingsDraftRole.icon,
canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
canReadAllObjectRecords:
settingsDraftRole.canReadAllObjectRecords,
canUpdateAllObjectRecords:
settingsDraftRole.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
settingsDraftRole.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords:
settingsDraftRole.canDestroyAllObjectRecords,
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
if (!data) {
return;
}
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: roleId,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords: objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
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) ?? ''],
});
}
}
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
await loadCurrentUser();
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.settingPermissions)) {
await upsertSettingPermissions({
variables: {
upsertSettingPermissionsInput: {
roleId: roleId,
settingPermissionKeys:
settingsDraftRole.settingPermissions?.map(
(settingPermission) => settingPermission.setting,
) ?? [],
},
},
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,
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 loadCurrentUser();
} finally {
setIsSaving(false);
}
};
return (
@ -292,7 +301,11 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
actionButton={
isRoleEditable &&
isDirty && (
<SaveAndCancelButtons onSave={handleSave} onCancel={handleCancel} />
<SaveAndCancelButtons
onSave={handleSave}
onCancel={handleCancel}
isLoading={isSaving}
/>
)
}
>
@ -311,7 +324,6 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
<SettingsRolePermissions
roleId={roleId}
isEditable={isRoleEditable}
isCreateMode={isCreateMode}
/>
)}
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS && (

View File

@ -1,5 +1,6 @@
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { t } from '@lingui/core/macro';
@ -16,6 +17,11 @@ export const SettingsRoleCreateEffect = ({
const setSettingsDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const setSettingsPersistedRole = useSetRecoilState(
settingsPersistedRoleFamilyState(roleId),
);
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId,
@ -44,9 +50,16 @@ export const SettingsRoleCreateEffect = ({
workspaceMembers: [],
};
setSettingsPersistedRole(undefined);
setSettingsDraftRole(newRole);
setIsInitialized(true);
}, [isInitialized, roleId, setActiveTabId, setSettingsDraftRole]);
}, [
isInitialized,
roleId,
setActiveTabId,
setSettingsDraftRole,
setSettingsPersistedRole,
]);
return null;
};

View File

@ -1,15 +1,16 @@
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
import { SettingsRole } from '@/settings/roles/role/components/SettingsRole';
import { SettingsRoleCreateEffect } from '@/settings/roles/role/components/SettingsRoleCreateEffect';
export const PENDING_ROLE_ID = 'pending-role-id';
import { v4 } from 'uuid';
export const SettingsRoleCreate = () => {
const newRoleId = v4();
return (
<>
<SettingsRolesQueryEffect />
<SettingsRoleCreateEffect roleId={PENDING_ROLE_ID} />
<SettingsRole roleId={PENDING_ROLE_ID} isCreateMode={true} />
<SettingsRoleCreateEffect roleId={newRoleId} />
<SettingsRole roleId={newRoleId} isCreateMode={true} />
</>
);
};

View File

@ -3,21 +3,29 @@ import { SettingsRole } from '@/settings/roles/role/components/SettingsRole';
import { SettingsRoleEditEffect } from '@/settings/roles/role/components/SettingsRoleEditEffect';
import { SettingsPath } from '@/types/SettingsPath';
import { Navigate, useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { settingsPersistedRoleFamilyState } from '~/modules/settings/roles/states/settingsPersistedRoleFamilyState';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsRoleEdit = () => {
const { roleId } = useParams();
const persistedRole = useRecoilValue(
settingsPersistedRoleFamilyState(roleId ?? ''),
);
if (!isDefined(roleId)) {
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
}
const isCreateMode = !isDefined(persistedRole?.id);
return (
<>
<SettingsRolesQueryEffect />
<SettingsRoleEditEffect roleId={roleId} />
<SettingsRole roleId={roleId} isCreateMode={false} />
<SettingsRole roleId={roleId} isCreateMode={isCreateMode} />
</>
);
};

View File

@ -67,6 +67,7 @@ export class RoleService {
await this.validateRoleInputOrThrow({ input, workspaceId });
const role = await this.roleRepository.save({
id: input.id,
label: input.label,
description: input.description,
icon: input.icon,