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:
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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 = ({
|
||||
|
||||
@ -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 ?? ''}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@ -2,7 +2,6 @@ export type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role?: { id: string; label: string };
|
||||
avatarUrl?: string | null;
|
||||
colorScheme?: string;
|
||||
userEmail?: string;
|
||||
};
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user