Add delete role action (#12691)
## Context Add delete role action, the backend takes care of most of the operations (can't delete a default role, can't delete the admin role, re-assign existing members to default role...) <img width="592" alt="Screenshot 2025-06-17 at 20 24 21" src="https://github.com/user-attachments/assets/3f01f12c-d8a4-466c-b4c7-9674f597a7a8" /> <img width="567" alt="Screenshot 2025-06-17 at 20 24 24" src="https://github.com/user-attachments/assets/8aceaf6c-3082-4ca6-a4dd-9767fc186923" />
This commit is contained in:
@ -2862,6 +2862,13 @@ export type CreateOneRoleMutationVariables = Exact<{
|
||||
|
||||
export type CreateOneRoleMutation = { __typename?: 'Mutation', createOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } };
|
||||
|
||||
export type DeleteOneRoleMutationVariables = Exact<{
|
||||
roleId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteOneRoleMutation = { __typename?: 'Mutation', deleteOneRole: string };
|
||||
|
||||
export type UpdateOneRoleMutationVariables = Exact<{
|
||||
updateRoleInput: UpdateRoleInput;
|
||||
}>;
|
||||
@ -5389,6 +5396,37 @@ export function useCreateOneRoleMutation(baseOptions?: Apollo.MutationHookOption
|
||||
export type CreateOneRoleMutationHookResult = ReturnType<typeof useCreateOneRoleMutation>;
|
||||
export type CreateOneRoleMutationResult = Apollo.MutationResult<CreateOneRoleMutation>;
|
||||
export type CreateOneRoleMutationOptions = Apollo.BaseMutationOptions<CreateOneRoleMutation, CreateOneRoleMutationVariables>;
|
||||
export const DeleteOneRoleDocument = gql`
|
||||
mutation DeleteOneRole($roleId: String!) {
|
||||
deleteOneRole(roleId: $roleId)
|
||||
}
|
||||
`;
|
||||
export type DeleteOneRoleMutationFn = Apollo.MutationFunction<DeleteOneRoleMutation, DeleteOneRoleMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteOneRoleMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteOneRoleMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteOneRoleMutation` 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 [deleteOneRoleMutation, { data, loading, error }] = useDeleteOneRoleMutation({
|
||||
* variables: {
|
||||
* roleId: // value for 'roleId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteOneRoleMutation(baseOptions?: Apollo.MutationHookOptions<DeleteOneRoleMutation, DeleteOneRoleMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteOneRoleMutation, DeleteOneRoleMutationVariables>(DeleteOneRoleDocument, options);
|
||||
}
|
||||
export type DeleteOneRoleMutationHookResult = ReturnType<typeof useDeleteOneRoleMutation>;
|
||||
export type DeleteOneRoleMutationResult = Apollo.MutationResult<DeleteOneRoleMutation>;
|
||||
export type DeleteOneRoleMutationOptions = Apollo.BaseMutationOptions<DeleteOneRoleMutation, DeleteOneRoleMutationVariables>;
|
||||
export const UpdateOneRoleDocument = gql`
|
||||
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
|
||||
updateOneRole(updateRoleInput: $updateRoleInput) {
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_ROLE = gql`
|
||||
mutation DeleteOneRole($roleId: String!) {
|
||||
deleteOneRole(roleId: $roleId)
|
||||
}
|
||||
`;
|
||||
@ -14,6 +14,7 @@ import {
|
||||
IconKey,
|
||||
IconLockOpen,
|
||||
IconSettings,
|
||||
IconSettingsAutomation,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
|
||||
@ -90,6 +91,12 @@ export const SettingsRolePermissionsSettingsSection = ({
|
||||
description: t`Manage security policies`,
|
||||
Icon: IconKey,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.WORKFLOWS,
|
||||
name: t`Workflows`,
|
||||
description: t`Manage workflows`,
|
||||
Icon: IconSettingsAutomation,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-settings/components/constants/RoleSettingsDeleteRoleConfirmationModalId';
|
||||
import { SettingsRoleSettingsDeleteRoleConfirmationModal } from '@/settings/roles/role-settings/components/SettingsRoleSettingsDeleteRoleConfirmationModal';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
@ -23,57 +28,85 @@ const StyledInputContainer = styled.div`
|
||||
type SettingsRoleSettingsProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
isCreateMode: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRoleSettings = ({
|
||||
roleId,
|
||||
isEditable,
|
||||
isCreateMode,
|
||||
}: SettingsRoleSettingsProps) => {
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledInputsContainer>
|
||||
<StyledInputContainer>
|
||||
<IconPicker
|
||||
selectedIconKey={settingsDraftRole.icon ?? 'IconUser'}
|
||||
dropdownId="role-settings-icon-picker"
|
||||
onChange={({ iconKey }: { iconKey: string }) => {
|
||||
<>
|
||||
<Section>
|
||||
<StyledInputsContainer>
|
||||
<StyledInputContainer>
|
||||
<IconPicker
|
||||
selectedIconKey={settingsDraftRole.icon ?? 'IconUser'}
|
||||
dropdownId="role-settings-icon-picker"
|
||||
onChange={({ iconKey }: { iconKey: string }) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
icon: iconKey,
|
||||
});
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
<TextInput
|
||||
value={settingsDraftRole.label}
|
||||
fullWidth
|
||||
onChange={(value: string) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
icon: iconKey,
|
||||
label: value,
|
||||
});
|
||||
}}
|
||||
placeholder={t`Role name`}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
<TextInput
|
||||
value={settingsDraftRole.label}
|
||||
fullWidth
|
||||
</StyledInputsContainer>
|
||||
<TextArea
|
||||
minRows={4}
|
||||
placeholder={t`Write a description`}
|
||||
value={settingsDraftRole.description || ''}
|
||||
onChange={(value: string) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
label: value,
|
||||
description: value,
|
||||
});
|
||||
}}
|
||||
placeholder={t`Role name`}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
<TextArea
|
||||
minRows={4}
|
||||
placeholder={t`Write a description`}
|
||||
value={settingsDraftRole.description || ''}
|
||||
onChange={(value: string) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
description: value,
|
||||
});
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
{!isCreateMode && (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Danger zone`}
|
||||
description={t`Delete this role and assign a new role to its members`}
|
||||
/>
|
||||
<Button
|
||||
title={t`Delete role`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
onClick={() => {
|
||||
openModal(ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</Section>
|
||||
<SettingsRoleSettingsDeleteRoleConfirmationModal roleId={roleId} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-settings/components/constants/RoleSettingsDeleteRoleConfirmationModalId';
|
||||
import { SettingsRoleSettingsDeleteRoleConfirmationModalSubtitle } from '@/settings/roles/role-settings/components/SettingsRoleSettingsDeleteRoleConfirmationModalSubtitle';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useDeleteOneRoleMutation } from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
type SettingsRoleSettingsDeleteRoleConfirmationModalProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleSettingsDeleteRoleConfirmationModal = ({
|
||||
roleId,
|
||||
}: SettingsRoleSettingsDeleteRoleConfirmationModalProps) => {
|
||||
const [deleteRole] = useDeleteOneRoleMutation();
|
||||
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const handleConfirmClick = async () => {
|
||||
await deleteRole({
|
||||
variables: { roleId },
|
||||
});
|
||||
navigateSettings(SettingsPath.Roles);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
modalId={ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID}
|
||||
title={t`Delete Role Permanently`}
|
||||
subtitle={
|
||||
<SettingsRoleSettingsDeleteRoleConfirmationModalSubtitle
|
||||
roleId={roleId}
|
||||
/>
|
||||
}
|
||||
onConfirmClick={handleConfirmClick}
|
||||
confirmButtonText={t`Confirm`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type SettingsRoleSettingsDeleteRoleConfirmationModalSubtitleProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleSettingsDeleteRoleConfirmationModalSubtitle = ({
|
||||
roleId,
|
||||
}: SettingsRoleSettingsDeleteRoleConfirmationModalSubtitleProps) => {
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
const roleName = settingsDraftRole.label;
|
||||
|
||||
return (
|
||||
<>{t`Confirm deletion of ${roleName} role? This cannot be undone. All members will be reassigned to the default role.`}</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export const ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID =
|
||||
'role-settings-delete-role-confirmation-modal';
|
||||
@ -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 SettingsRoleSettingsWrapper = (
|
||||
args: React.ComponentProps<typeof SettingsRoleSettings>,
|
||||
@ -22,7 +22,11 @@ const SettingsRoleSettingsWrapper = (
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsRoleSettings roleId={args.roleId} isEditable={args.isEditable} />
|
||||
<SettingsRoleSettings
|
||||
roleId={args.roleId}
|
||||
isEditable={args.isEditable}
|
||||
isCreateMode={args.isCreateMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -39,6 +43,7 @@ export const Default: Story = {
|
||||
args: {
|
||||
roleId: '1',
|
||||
isEditable: true,
|
||||
isCreateMode: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -46,11 +51,22 @@ export const ReadOnly: Story = {
|
||||
args: {
|
||||
roleId: '1',
|
||||
isEditable: false,
|
||||
isCreateMode: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingRole: Story = {
|
||||
args: {
|
||||
roleId: PENDING_ROLE_ID,
|
||||
isEditable: true,
|
||||
isCreateMode: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const CreateMode: Story = {
|
||||
args: {
|
||||
roleId: '1',
|
||||
isEditable: true,
|
||||
isCreateMode: true,
|
||||
},
|
||||
};
|
||||
|
||||
@ -205,6 +205,21 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
},
|
||||
});
|
||||
} 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: {
|
||||
@ -229,21 +244,6 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(dirtyFields.settingPermissions)) {
|
||||
await upsertSettingPermissions({
|
||||
variables: {
|
||||
upsertSettingPermissionsInput: {
|
||||
roleId: roleId,
|
||||
settingPermissionKeys:
|
||||
settingsDraftRole.settingPermissions?.map(
|
||||
(settingPermission) => settingPermission.setting,
|
||||
) ?? [],
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(dirtyFields.objectPermissions)) {
|
||||
await upsertObjectPermissions({
|
||||
variables: {
|
||||
@ -310,7 +310,11 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
/>
|
||||
)}
|
||||
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS && (
|
||||
<SettingsRoleSettings roleId={roleId} isEditable={isRoleEditable} />
|
||||
<SettingsRoleSettings
|
||||
roleId={roleId}
|
||||
isEditable={isRoleEditable}
|
||||
isCreateMode={isCreateMode}
|
||||
/>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
|
||||
@ -2,6 +2,7 @@ import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/Setti
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
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';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
@ -31,7 +32,7 @@ export const SettingsRoleCreateEffect = ({
|
||||
|
||||
const newRole = {
|
||||
id: roleId,
|
||||
label: '',
|
||||
label: t`Role name`,
|
||||
description: '',
|
||||
icon: 'IconUser',
|
||||
canUpdateAllSettings: true,
|
||||
|
||||
@ -4,11 +4,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([SettingPermissionEntity, RoleEntity], 'core'),
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
|
||||
providers: [SettingPermissionService],
|
||||
exports: [SettingPermissionService],
|
||||
})
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UpsertSettingPermissionsInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
export class SettingPermissionService {
|
||||
constructor(
|
||||
@ -21,6 +22,7 @@ export class SettingPermissionService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectDataSource('core')
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async upsertSettingPermissions({
|
||||
@ -115,6 +117,14 @@ export class SettingPermissionService {
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
roleIds: [input.roleId],
|
||||
ignoreLock: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user