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:
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user