add role update (#11217)

## Context
This PR introduces the new Create and Edit role components, behind the
PERMISSIONS_ENABLED_V2 feature flag.
This commit is contained in:
Weiko
2025-03-31 17:57:14 +02:00
committed by GitHub
parent 3c9bf2294f
commit 06ff16e086
58 changed files with 1527 additions and 624 deletions

View File

@ -0,0 +1,198 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment';
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
import { SettingsRoleSettings } from '@/settings/roles/role-settings/components/SettingsRoleSettings';
import { SettingsRoleLabelContainer } from '@/settings/roles/role/components/SettingsRoleLabelContainer';
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 { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Button, IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui';
import { v4 } from 'uuid';
import {
FeatureFlagKey,
useCreateOneRoleMutation,
useUpdateOneRoleMutation,
} from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsRoleProps = {
roleId: string;
isCreateMode: boolean;
};
export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID,
);
const isPermissionsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
);
const navigateSettings = useNavigateSettings();
const [createRole] = useCreateOneRoleMutation();
const [updateRole] = useUpdateOneRoleMutation();
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const settingsPersistedRole = useRecoilValue(
settingsPersistedRoleFamilyState(roleId),
);
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
if (!isDefined(settingsRolesIsLoading)) {
return <></>;
}
const isRoleEditable = isPermissionsV2Enabled && settingsDraftRole.isEditable;
const tabs = [
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT,
title: t`Assignment`,
Icon: IconUserPlus,
},
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS,
title: t`Permissions`,
Icon: IconLockOpen,
},
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS,
title: t`Settings`,
Icon: IconSettings,
},
];
const isDirty = !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
const handleSave = () => {
if (isCreateMode) {
const roleId = v4();
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,
},
},
onCompleted: async (data) => {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
navigateSettings(SettingsPath.RoleDetail, {
roleId: data.createOneRole.id,
});
},
});
} else {
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,
},
},
},
});
}
};
return (
<SubMenuTopBarContainer
title={<SettingsRoleLabelContainer roleId={roleId} />}
links={[
{
children: 'Workspace',
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Roles',
href: getSettingsPath(SettingsPath.Roles),
},
{
children: settingsDraftRole.label,
},
]}
actionButton={
isDirty && (
<Button
title={isCreateMode ? t`Create` : t`Save`}
variant="primary"
size="small"
accent="blue"
onClick={handleSave}
disabled={!isRoleEditable}
/>
)
}
>
<SettingsPageContainer>
<TabList
tabs={tabs}
className="tab-list"
componentInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID}
/>
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT && (
<SettingsRoleAssignment roleId={roleId} isCreateMode={isCreateMode} />
)}
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS && (
<SettingsRolePermissions
roleId={roleId}
isEditable={isRoleEditable}
/>
)}
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS && (
<SettingsRoleSettings roleId={roleId} isEditable={isRoleEditable} />
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,45 @@
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
export const SettingsRoleCreateEffect = ({ roleId }: { roleId: string }) => {
const setSettingsDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID,
);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
if (isInitialized) {
return;
}
setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS);
const newRole = {
id: roleId,
label: '',
description: '',
icon: 'IconUser',
canUpdateAllSettings: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
isEditable: true,
workspaceMembers: [],
};
setSettingsDraftRole(newRole);
setIsInitialized(true);
}, [isInitialized, roleId, setActiveTabId, setSettingsDraftRole]);
return null;
};

View File

@ -0,0 +1,40 @@
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/states/activeTabIdComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
type SettingsRoleEditEffectProps = {
roleId: string;
};
export const SettingsRoleEditEffect = ({
roleId,
}: SettingsRoleEditEffectProps) => {
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,
);
useEffect(() => {
if (isInitialized) {
return;
}
setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT);
if (isDefined(role)) {
setDraftRole(role);
setIsInitialized(true);
}
}, [isInitialized, role, setActiveTabId, setDraftRole]);
return <></>;
};

View File

@ -0,0 +1,50 @@
import { useRecoilState } from 'recoil';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { TitleInput } from '@/ui/input/components/TitleInput';
import styled from '@emotion/styled';
type SettingsRoleLabelContainerProps = {
roleId: string;
};
const ROLE_LABEL_EDIT_HOTKEY_SCOPE = 'role-label-edit';
const StyledHeaderTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
font-size: ${({ theme }) => theme.font.size.lg};
width: fit-content;
max-width: 420px;
& > input:disabled {
color: ${({ theme }) => theme.font.color.primary};
}
`;
export const SettingsRoleLabelContainer = ({
roleId,
}: SettingsRoleLabelContainerProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const handleChange = (newValue: string) => {
setSettingsDraftRole({
...settingsDraftRole,
label: newValue,
});
};
return (
<StyledHeaderTitle>
<TitleInput
disabled={!settingsDraftRole.isEditable}
sizeVariant="md"
value={settingsDraftRole.label}
onChange={handleChange}
placeholder="Role name"
hotkeyScope={ROLE_LABEL_EDIT_HOTKEY_SCOPE}
/>
</StyledHeaderTitle>
);
};