Role page various fixes 2 (#12416)

- Fix: AvatarURL signedPath for workspace members were not consistent
when queried multiple times and it was causing the frontend to wrongly
interpret this as a change in the deepEqual condition
- Use SaveAndCancel button to be consistent with data model page
- When applying all object permission changes, a "smarter" logic applies
and removes all permissions if read is unchecked for example
- Hide settings permissions when Settings All Access is toggled
This commit is contained in:
Weiko
2025-06-02 20:24:53 +02:00
committed by GitHub
parent e1a7fa3e5d
commit 8e710004ba
13 changed files with 139 additions and 69 deletions

View File

@ -28,11 +28,21 @@ export const SettingsRolesQueryEffect = () => {
snapshot,
settingsPersistedRoleFamilyState(role.id),
);
const currentDraftRole = getSnapshotValue(
snapshot,
settingsDraftRoleFamilyState(role.id),
);
if (isDeeplyEqual(role, persistedRole)) {
return;
}
set(settingsDraftRoleFamilyState(role.id), role);
set(settingsPersistedRoleFamilyState(role.id), role);
if (!isDeeplyEqual(currentDraftRole, role)) {
set(settingsDraftRoleFamilyState(role.id), role);
}
});
},
[],

View File

@ -1,9 +1,12 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
AppTooltip,
Avatar,
@ -62,6 +65,16 @@ export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
const { getIcon } = useIcons();
const Icon = getIcon(role.icon ?? 'IconUser');
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const enrichedWorkspaceMembers = role.workspaceMembers
.map((workspaceMember) =>
currentWorkspaceMembers.find(
(member) => member.id === workspaceMember.id,
),
)
.filter(isDefined);
return (
<StyledTableRow
key={role.id}
@ -85,7 +98,7 @@ export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
</TableCell>
<TableCell align={'right'}>
<StyledAvatarGroup>
{role.workspaceMembers.slice(0, 5).map((workspaceMember) => (
{enrichedWorkspaceMembers.slice(0, 5).map((workspaceMember) => (
<React.Fragment key={workspaceMember.id}>
<div id={`avatar-${workspaceMember.id}`}>
<Avatar

View File

@ -1,4 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import {
CurrentWorkspaceMember,
currentWorkspaceMemberState,
} from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { SettingsRoleAssignmentConfirmationModal } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModal';
@ -28,11 +31,7 @@ import {
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
Role,
SearchRecord,
WorkspaceMember,
} from '~/generated-metadata/graphql';
import { Role, WorkspaceMember } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '../constants/RoleAssignmentConfirmationModalId';
import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow';
@ -142,17 +141,14 @@ export const SettingsRoleAssignment = ({
};
const handleSelectWorkspaceMember = (
workspaceMemberSearchRecord: SearchRecord,
workspaceMember: CurrentWorkspaceMember,
) => {
const existingRole = workspaceMemberRoleMap.get(
workspaceMemberSearchRecord.recordId,
);
const existingRole = workspaceMemberRoleMap.get(workspaceMember.id);
setSelectedWorkspaceMember({
id: workspaceMemberSearchRecord.recordId,
name: `${workspaceMemberSearchRecord.label}`,
id: workspaceMember.id,
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
role: existingRole,
avatarUrl: workspaceMemberSearchRecord.imageUrl,
});
openModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
closeDropdown();

View File

@ -1,7 +1,9 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { Avatar } from 'twenty-ui/display';
const StyledSettingsCardContainer = styled.div`
@ -17,7 +19,13 @@ export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
selectedWorkspaceMember,
onRoleClick,
}: SettingsRoleAssignmentConfirmationModalSubtitleProps) => {
const workspaceMemberName = selectedWorkspaceMember.name;
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const enrichedSelectedWorkspaceMember = currentWorkspaceMembers.find(
(member) => member.id === selectedWorkspaceMember.id,
);
const workspaceMemberName = `${enrichedSelectedWorkspaceMember?.name.firstName} ${enrichedSelectedWorkspaceMember?.name.lastName}`;
return (
<>
@ -27,9 +35,9 @@ export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
title={selectedWorkspaceMember.role?.label || ''}
Icon={
<Avatar
avatarUrl={selectedWorkspaceMember.avatarUrl}
placeholderColorSeed={selectedWorkspaceMember.id}
placeholder={selectedWorkspaceMember.name}
avatarUrl={enrichedSelectedWorkspaceMember?.avatarUrl}
placeholderColorSeed={enrichedSelectedWorkspaceMember?.id}
placeholder={workspaceMemberName}
size="md"
type="rounded"
/>

View File

@ -1,6 +1,8 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { WorkspaceMember } from '~/generated-metadata/graphql';
@ -35,15 +37,20 @@ type SettingsRoleAssignmentTableRowProps = {
export const SettingsRoleAssignmentTableRow = ({
workspaceMember,
}: SettingsRoleAssignmentTableRowProps) => {
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const enrichedWorkspaceMember = currentWorkspaceMembers.find(
(member) => member.id === workspaceMember.id,
);
return (
<TableRow gridAutoColumns="2fr 4fr">
<StyledTableCell>
<StyledNameContainer>
<StyledIconWrapper>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={workspaceMember.name.firstName ?? ''}
avatarUrl={enrichedWorkspaceMember?.avatarUrl}
placeholderColorSeed={enrichedWorkspaceMember?.id}
placeholder={enrichedWorkspaceMember?.name.firstName ?? ''}
type="rounded"
size="md"
/>

View File

@ -1,3 +1,4 @@
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent';
@ -7,11 +8,10 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useLingui } from '@lingui/react/macro';
import { ChangeEvent, useState } from 'react';
import { SearchRecord } from '~/generated-metadata/graphql';
type SettingsRoleAssignmentWorkspaceMemberPickerDropdownProps = {
excludedWorkspaceMemberIds: string[];
onSelect: (workspaceMemberSearchRecord: SearchRecord) => void;
onSelect: (workspaceMember: CurrentWorkspaceMember) => void;
};
export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({

View File

@ -1,4 +1,8 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { MenuItem, MenuItemAvatar } from 'twenty-ui/navigation';
import { SearchRecord } from '~/generated-metadata/graphql';
@ -6,7 +10,7 @@ type SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
loading: boolean;
searchFilter: string;
filteredWorkspaceMembers: SearchRecord[];
onSelect: (workspaceMemberSearchRecord: SearchRecord) => void;
onSelect: (workspaceMember: CurrentWorkspaceMember) => void;
};
export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
@ -15,6 +19,8 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
filteredWorkspaceMembers,
onSelect,
}: SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps) => {
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
if (loading) {
return null;
}
@ -23,20 +29,28 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
return <MenuItem disabled text={t`No Results`} />;
}
const enrichedWorkspaceMembers = filteredWorkspaceMembers
.map((workspaceMember) =>
currentWorkspaceMembers.find(
(member) => member.id === workspaceMember.recordId,
),
)
.filter(isDefined);
return (
<>
{filteredWorkspaceMembers.map((workspaceMember) => (
{enrichedWorkspaceMembers.map((workspaceMember) => (
<MenuItemAvatar
key={workspaceMember.recordId}
key={workspaceMember.id}
onClick={() => onSelect(workspaceMember)}
avatar={{
type: 'rounded',
size: 'md',
placeholder: workspaceMember.label ?? '',
placeholderColorSeed: workspaceMember.recordId,
avatarUrl: workspaceMember.imageUrl,
placeholder: workspaceMember?.name.firstName ?? '',
placeholderColorSeed: workspaceMember?.id,
avatarUrl: workspaceMember?.avatarUrl,
}}
text={workspaceMember.label}
text={workspaceMember?.name.firstName ?? ''}
/>
))}
</>

View File

@ -2,7 +2,6 @@ export type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember = {
id: string;
name: string;
role?: { id: string; label: string };
avatarUrl?: string | null;
colorScheme?: string;
userEmail?: string;
};

View File

@ -82,7 +82,13 @@ export const SettingsRolePermissionsObjectLevelSection = ({
...(draftRole.objectPermissions ?? []).filter(
(permission) => permission.objectMetadataId !== objectMetadataId,
),
{ objectMetadataId, roleId },
{
objectMetadataId,
canReadObjectRecords: null,
canUpdateObjectRecords: null,
canSoftDeleteObjectRecords: null,
canDestroyObjectRecords: null,
},
],
}));
navigate(SettingsPath.RoleObjectLevel, {

View File

@ -47,6 +47,13 @@ export const SettingsRolePermissionsObjectsSection = ({
setSettingsDraftRole({
...settingsDraftRole,
canReadAllObjectRecords: value,
...(value === false
? {
canUpdateAllObjectRecords: value,
canSoftDeleteAllObjectRecords: value,
canDestroyAllObjectRecords: value,
}
: {}),
});
},
},
@ -64,6 +71,11 @@ export const SettingsRolePermissionsObjectsSection = ({
setSettingsDraftRole({
...settingsDraftRole,
canUpdateAllObjectRecords: value,
...(value === true
? {
canReadAllObjectRecords: value,
}
: {}),
});
},
},
@ -81,6 +93,11 @@ export const SettingsRolePermissionsObjectsSection = ({
setSettingsDraftRole({
...settingsDraftRole,
canSoftDeleteAllObjectRecords: value,
...(value === true
? {
canReadAllObjectRecords: value,
}
: {}),
});
},
},
@ -98,6 +115,11 @@ export const SettingsRolePermissionsObjectsSection = ({
setSettingsDraftRole({
...settingsDraftRole,
canDestroyAllObjectRecords: value,
...(value === true
? {
canReadAllObjectRecords: value,
}
: {}),
});
},
},

View File

@ -16,7 +16,7 @@ import {
IconSettings,
IconUsers,
} from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
import {
FeatureFlagKey,
SettingPermissionType,
@ -112,19 +112,30 @@ export const SettingsRolePermissionsSettingsSection = ({
/>
</StyledCard>
)}
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader />
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
roleId={roleId}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
<AnimatedExpandableContainer
isExpanded={!settingsDraftRole.canUpdateAllSettings}
dimension="height"
animationDurations={{
opacity: 0.2,
size: 0.4,
}}
mode="scroll-height"
containAnimation={false}
>
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader />
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
roleId={roleId}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</AnimatedExpandableContainer>
</Section>
);
};

View File

@ -1,3 +1,4 @@
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
@ -22,7 +23,6 @@ import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { v4 } from 'uuid';
import {
FeatureFlagKey,
@ -281,16 +281,11 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
},
]}
actionButton={
isDirty && (
<Button
title={isCreateMode ? t`Create` : t`Save`}
variant="primary"
size="small"
accent="blue"
onClick={handleSave}
disabled={!isRoleEditable}
/>
)
<SaveAndCancelButtons
onSave={handleSave}
onCancel={() => navigateSettings(SettingsPath.Roles)}
isSaveDisabled={!isRoleEditable || !isDirty}
/>
}
>
<SettingsPageContainer>

View File

@ -188,17 +188,6 @@ export class RoleResolver {
workspace.id,
);
await Promise.all(
workspaceMembers.map(async (workspaceMember) => {
if (workspaceMember && workspaceMember.avatarUrl) {
workspaceMember.avatarUrl = this.fileService.signFileUrl({
url: workspaceMember.avatarUrl,
workspaceId: workspace.id,
});
}
}),
);
return workspaceMembers;
}