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
@@ -1,8 +1,12 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { RoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/RoleAssignmentTableHeader';
import { RoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown';
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { SettingsRoleAssignmentConfirmationModal } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModal';
import { SettingsRoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader';
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown';
import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -26,14 +30,8 @@ import {
SearchRecord,
WorkspaceMember,
} from '~/generated-metadata/graphql';
import {
GetRolesDocument,
useGetRolesQuery,
useUpdateWorkspaceMemberRoleMutation,
} from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal';
import { RoleAssignmentTableRow } from './RoleAssignmentTableRow';
import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow';
const StyledAssignToMemberContainer = styled.div`
display: flex;
@@ -67,43 +65,51 @@ const StyledNoMembers = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
type RoleAssignmentProps = {
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & {
workspaceMembers: Array<WorkspaceMember>;
};
type SettingsRoleAssignmentProps = {
roleId: string;
isCreateMode?: boolean;
};
export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
export const SettingsRoleAssignment = ({
roleId,
isCreateMode,
}: SettingsRoleAssignmentProps) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const navigateSettings = useNavigateSettings();
const [updateWorkspaceMemberRole] = useUpdateWorkspaceMemberRoleMutation({
refetchQueries: [GetRolesDocument],
});
const {
addWorkspaceMemberToRoleAndUpdateState,
updateWorkspaceMemberRoleDraftState,
} = useUpdateWorkspaceMemberRole(roleId);
const [confirmationModalIsOpen, setConfirmationModalIsOpen] =
useState<boolean>(false);
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
useState<SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
null,
);
const { data: rolesData } = useGetRolesQuery();
const { closeDropdown } = useDropdown('role-member-select');
const [searchFilter, setSearchFilter] = useState('');
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
const workspaceMemberRoleMap = new Map<
string,
{ id: string; label: string }
>();
rolesData?.getRoles?.forEach((role) => {
role.workspaceMembers.forEach((member) => {
settingsAllRoles.forEach((role: Role) => {
role.workspaceMembers.forEach((member: WorkspaceMember) => {
workspaceMemberRoleMap.set(member.id, { id: role.id, label: role.label });
});
});
const filteredWorkspaceMembers = !searchFilter
? role.workspaceMembers
: role.workspaceMembers.filter((member) => {
? settingsDraftRole.workspaceMembers
: settingsDraftRole.workspaceMembers.filter((member) => {
const searchTerm = searchFilter.toLowerCase();
const firstName = member.name.firstName?.toLowerCase() || '';
const lastName = member.name.lastName?.toLowerCase() || '';
@@ -116,7 +122,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
);
});
const assignedWorkspaceMemberIds = role.workspaceMembers.map(
const assignedWorkspaceMemberIds = settingsDraftRole.workspaceMembers.map(
(workspaceMember) => workspaceMember.id,
);
@@ -153,12 +159,28 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
const handleConfirm = async () => {
if (!selectedWorkspaceMember || !confirmationModalIsOpen) return;
await updateWorkspaceMemberRole({
variables: {
if (!isCreateMode) {
await addWorkspaceMemberToRoleAndUpdateState({
workspaceMemberId: selectedWorkspaceMember.id,
roleId: role.id,
},
});
});
} else {
const workspaceMember = currentWorkspaceMembers.find(
(member) => member.id === selectedWorkspaceMember.id,
);
if (!workspaceMember) {
throw new Error('Workspace member not found');
}
updateWorkspaceMemberRoleDraftState({
workspaceMember: {
id: workspaceMember.id,
name: workspaceMember.name,
colorScheme: '',
userEmail: '',
},
});
}
handleModalClose();
};
@@ -190,11 +212,11 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
/>
</StyledSearchContainer>
<StyledTable>
<RoleAssignmentTableHeader />
<SettingsRoleAssignmentTableHeader />
<StyledTableRows>
{filteredWorkspaceMembers.length > 0 ? (
filteredWorkspaceMembers.map((workspaceMember) => (
<RoleAssignmentTableRow
<SettingsRoleAssignmentTableRow
key={workspaceMember.id}
workspaceMember={workspaceMember}
/>
@@ -233,7 +255,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
</>
}
dropdownComponents={
<RoleAssignmentWorkspaceMemberPickerDropdown
<SettingsRoleAssignmentWorkspaceMemberPickerDropdown
excludedWorkspaceMemberIds={[
...assignedWorkspaceMemberIds,
currentWorkspaceMember?.id,
@@ -246,7 +268,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
</Section>
{confirmationModalIsOpen && selectedWorkspaceMember && (
<RoleAssignmentConfirmationModal
<SettingsRoleAssignmentConfirmationModal
selectedWorkspaceMember={selectedWorkspaceMember}
isOpen={true}
onClose={handleModalClose}
@@ -1,23 +1,23 @@
import { RoleAssignmentConfirmationModalSubtitle } from '@/settings/roles/role-assignment/components/RoleAssignmentConfirmationModalSubtitle';
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { SettingsRoleAssignmentConfirmationModalSubtitle } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle';
import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { t } from '@lingui/core/macro';
type RoleAssignmentConfirmationModalProps = {
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
type SettingsRoleAssignmentConfirmationModalProps = {
selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember;
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
onRoleClick: (roleId: string) => void;
};
export const RoleAssignmentConfirmationModal = ({
export const SettingsRoleAssignmentConfirmationModal = ({
selectedWorkspaceMember,
isOpen,
onClose,
onConfirm,
onRoleClick,
}: RoleAssignmentConfirmationModalProps) => {
}: SettingsRoleAssignmentConfirmationModalProps) => {
const workspaceMemberName = selectedWorkspaceMember.name;
const title = t`Assign ${workspaceMemberName}?`;
@@ -28,7 +28,7 @@ export const RoleAssignmentConfirmationModal = ({
setIsOpen={onClose}
title={title}
subtitle={
<RoleAssignmentConfirmationModalSubtitle
<SettingsRoleAssignmentConfirmationModalSubtitle
selectedWorkspaceMember={selectedWorkspaceMember}
onRoleClick={onRoleClick}
/>
@@ -1,5 +1,5 @@
import { SettingsCard } from '@/settings/components/SettingsCard';
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Avatar } from 'twenty-ui';
@@ -8,15 +8,15 @@ const StyledSettingsCardContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(6)};
`;
type RoleAssignmentConfirmationModalSubtitleProps = {
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
type SettingsRoleAssignmentConfirmationModalSubtitleProps = {
selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember;
onRoleClick: (roleId: string) => void;
};
export const RoleAssignmentConfirmationModalSubtitle = ({
export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
selectedWorkspaceMember,
onRoleClick,
}: RoleAssignmentConfirmationModalSubtitleProps) => {
}: SettingsRoleAssignmentConfirmationModalSubtitleProps) => {
const workspaceMemberName = selectedWorkspaceMember.name;
return (
@@ -2,7 +2,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
export const RoleAssignmentTableHeader = () => (
export const SettingsRoleAssignmentTableHeader = () => (
<TableRow gridAutoColumns="2fr 4fr">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader>
@@ -28,13 +28,13 @@ const StyledTableCell = styled(TableCell)`
overflow: hidden;
`;
type RoleAssignmentTableRowProps = {
type SettingsRoleAssignmentTableRowProps = {
workspaceMember: WorkspaceMember;
};
export const RoleAssignmentTableRow = ({
export const SettingsRoleAssignmentTableRow = ({
workspaceMember,
}: RoleAssignmentTableRowProps) => {
}: SettingsRoleAssignmentTableRowProps) => {
return (
<TableRow gridAutoColumns="2fr 4fr">
<StyledTableCell>
@@ -1,6 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
import { RoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent';
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@@ -9,15 +9,15 @@ import { useLingui } from '@lingui/react/macro';
import { ChangeEvent, useState } from 'react';
import { SearchRecord } from '~/generated-metadata/graphql';
type RoleAssignmentWorkspaceMemberPickerDropdownProps = {
type SettingsRoleAssignmentWorkspaceMemberPickerDropdownProps = {
excludedWorkspaceMemberIds: string[];
onSelect: (workspaceMemberSearchRecord: SearchRecord) => void;
};
export const RoleAssignmentWorkspaceMemberPickerDropdown = ({
export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({
excludedWorkspaceMemberIds,
onSelect,
}: RoleAssignmentWorkspaceMemberPickerDropdownProps) => {
}: SettingsRoleAssignmentWorkspaceMemberPickerDropdownProps) => {
const [searchFilter, setSearchFilter] = useState('');
const { loading, searchRecords: workspaceMembers } =
@@ -46,7 +46,7 @@ export const RoleAssignmentWorkspaceMemberPickerDropdown = ({
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<RoleAssignmentWorkspaceMemberPickerDropdownContent
<SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent
loading={loading}
searchFilter={searchFilter}
filteredWorkspaceMembers={filteredWorkspaceMembers}
@@ -2,19 +2,19 @@ import { t } from '@lingui/core/macro';
import { MenuItem, MenuItemAvatar } from 'twenty-ui';
import { SearchRecord } from '~/generated-metadata/graphql';
type RoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
type SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
loading: boolean;
searchFilter: string;
filteredWorkspaceMembers: SearchRecord[];
onSelect: (workspaceMemberSearchRecord: SearchRecord) => void;
};
export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({
export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
loading,
searchFilter,
filteredWorkspaceMembers,
onSelect,
}: RoleAssignmentWorkspaceMemberPickerDropdownContentProps) => {
}: SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps) => {
if (loading) {
return null;
}
@@ -0,0 +1,46 @@
import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
const SettingsRoleAssignmentWrapper = (
args: React.ComponentProps<typeof SettingsRoleAssignment>,
) => {
const setDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(args.roleId),
);
const role = getRolesMock().find((role) => role.id === args.roleId);
if (isDefined(role)) {
setDraftRole(role);
}
return <SettingsRoleAssignment roleId={args.roleId} />;
};
const meta: Meta<typeof SettingsRoleAssignmentWrapper> = {
title: 'Modules/Settings/Roles/RoleAssignment/SettingsRoleAssignment',
component: SettingsRoleAssignmentWrapper,
decorators: [RouterDecorator, ComponentDecorator, I18nFrontDecorator],
};
export default meta;
type Story = StoryObj<typeof SettingsRoleAssignmentWrapper>;
export const Default: Story = {
args: {
roleId: '1',
},
};
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
},
};
@@ -1,6 +0,0 @@
export type RoleAssignmentConfirmationModalSelectedWorkspaceMember = {
id: string;
name: string;
role?: { id: string; label: string };
avatarUrl?: string | null;
};
@@ -0,0 +1,8 @@
export type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember = {
id: string;
name: string;
role?: { id: string; label: string };
avatarUrl?: string | null;
colorScheme?: string;
userEmail?: string;
};