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:
Weiko
2025-06-18 00:43:23 +02:00
committed by GitHub
parent 338f08b3a4
commit 6650d4b059
12 changed files with 230 additions and 48 deletions

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_ROLE = gql`
mutation DeleteOneRole($roleId: String!) {
deleteOneRole(roleId: $roleId)
}
`;

View File

@ -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 (

View File

@ -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} />
</>
)}
</>
);
};

View File

@ -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"
/>
);
};

View File

@ -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.`}</>
);
};

View File

@ -0,0 +1,2 @@
export const ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID =
'role-settings-delete-role-confirmation-modal';

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 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,
},
};

View File

@ -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>

View File

@ -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,