[Permissions][FE] Design followup 5 (#12793)

## Context
- We now display workspace member full name and email in role assignment
picker
- Replaced Forbidden by "Not shared" with lock icon
- Fix Disabled for Danger accent
- Fix avatar URL

<img width="575" alt="Screenshot 2025-06-23 at 16 38 56"
src="https://github.com/user-attachments/assets/08430bfe-29c4-4ac4-821c-9062dfad7150"
/>
<img width="756" alt="Screenshot 2025-06-23 at 16 21 36"
src="https://github.com/user-attachments/assets/c19f31bd-fe9d-415d-aa55-62fa3e228c49"
/>
<img width="373" alt="Screenshot 2025-06-23 at 17 13 08"
src="https://github.com/user-attachments/assets/e2f7878c-7c5a-40b4-a482-8e99292257c3"
/>
<img width="342" alt="Screenshot 2025-06-23 at 17 37 49"
src="https://github.com/user-attachments/assets/04169e85-14dd-4aed-bd71-7aefd601a894"
/>
<img width="434" alt="Screenshot 2025-06-23 at 17 37 35"
src="https://github.com/user-attachments/assets/7caf0967-c4dd-4d0f-90c8-259a85182b19"
/>
This commit is contained in:
Weiko
2025-06-23 19:15:58 +02:00
committed by GitHub
parent f05da75bb5
commit 85c50f149d
16 changed files with 70 additions and 45 deletions

View File

@ -30,6 +30,7 @@ const mockWorkspaceMember = {
lastName: 'Doe',
},
colorScheme: 'Light' as const,
userEmail: 'userEmail',
};
const mockWorkspace = {
@ -200,6 +201,7 @@ describe('ApolloFactory', () => {
lastName: 'Doe',
},
colorScheme: 'Light' as const,
userEmail: 'userEmail',
};
apolloFactory.updateWorkspaceMember(newWorkspaceMember);

View File

@ -3,7 +3,7 @@ import { createState } from 'twenty-ui/utilities';
export type CurrentWorkspaceMember = Omit<
WorkspaceMember,
'createdAt' | 'updatedAt' | 'userId' | 'userEmail' | '__typename'
'createdAt' | 'updatedAt' | 'userId' | '__typename'
>;
export const currentWorkspaceMemberState =

View File

@ -1012,4 +1012,5 @@ export const mockWorkspaceMember = {
createdAt: '',
updatedAt: '',
userId: '1',
userEmail: 'userEmail',
};

View File

@ -43,6 +43,7 @@ describe('useFindManyRecords', () => {
name: { firstName: 'John', lastName: 'Connor' },
locale: 'en',
colorScheme: 'Light',
userEmail: 'userEmail',
});
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);

View File

@ -1,19 +1,20 @@
import { Theme, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { IconLock } from 'twenty-ui/display';
const StyledContainer = styled.div<{ theme: Theme }>`
align-items: center;
display: flex;
display: inline-flex;
background: ${({ theme }) => theme.background.transparent.lighter};
background: ${({ theme }) => theme.background.transparent.light};
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.regular};
font-size: ${({ theme }) => theme.font.size.sm};
padding: ${({ theme }) => theme.spacing(1, 2)};
font-size: ${({ theme }) => theme.font.size.md};
padding: ${({ theme }) => theme.spacing(1)};
gap: ${({ theme }) => theme.spacing(1)};
border-radius: 4px;
border: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const ForbiddenFieldDisplay = () => {
@ -21,7 +22,8 @@ export const ForbiddenFieldDisplay = () => {
return (
<StyledContainer theme={theme}>
<Trans>Forbidden</Trans>
<IconLock size={theme.icon.size.sm} />
<Trans>Not shared</Trans>
</StyledContainer>
);
};

View File

@ -229,6 +229,7 @@ describe('buildValueFromFilter', () => {
dateFormat: null,
timeFormat: null,
timeZone: null,
userEmail: 'userEmail',
};
const testCases = [

View File

@ -74,6 +74,7 @@ describe('useFilteredSearchRecordQuery', () => {
name: { firstName: 'John', lastName: 'Connor' },
locale: 'en',
colorScheme: 'Light',
userEmail: 'userEmail',
});
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);

View File

@ -6,6 +6,7 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useLingui } from '@lingui/react/macro';
import { ChangeEvent, useState } from 'react';
@ -38,7 +39,7 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({
const { t } = useLingui();
return (
<DropdownContent>
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}

View File

@ -39,20 +39,25 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
return (
<>
{enrichedWorkspaceMembers.map((workspaceMember) => (
<MenuItemAvatar
key={workspaceMember.id}
onClick={() => onSelect(workspaceMember)}
avatar={{
type: 'rounded',
size: 'md',
placeholder: workspaceMember?.name.firstName ?? '',
placeholderColorSeed: workspaceMember?.id,
avatarUrl: workspaceMember?.avatarUrl,
}}
text={workspaceMember?.name.firstName ?? ''}
/>
))}
{enrichedWorkspaceMembers.map((workspaceMember) => {
const workspaceMemberFullName = `${workspaceMember?.name.firstName ?? ''} ${workspaceMember?.name.lastName ?? ''}`;
return (
<MenuItemAvatar
key={workspaceMember.id}
onClick={() => onSelect(workspaceMember)}
avatar={{
type: 'rounded',
size: 'md',
placeholder: workspaceMemberFullName,
placeholderColorSeed: workspaceMember.id,
avatarUrl: workspaceMember.avatarUrl,
}}
text={workspaceMemberFullName}
contextualText={workspaceMember.userEmail}
/>
);
})}
</>
);
};

View File

@ -10,6 +10,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@ -77,7 +78,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
const { t } = useLingui();
return (
<DropdownContent widthInPixels={320}>
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent

View File

@ -10,6 +10,7 @@ import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
@ -78,7 +79,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
);
return (
<DropdownContent widthInPixels={320}>
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent

View File

@ -15,7 +15,7 @@ jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
const workspaceMember: Omit<
WorkspaceMember,
'createdAt' | 'updatedAt' | 'userId' | 'userEmail'
'createdAt' | 'updatedAt' | 'userId'
> = {
__typename: 'WorkspaceMember',
id: 'id',
@ -25,6 +25,7 @@ const workspaceMember: Omit<
},
locale: 'en',
colorScheme: 'System',
userEmail: 'userEmail',
};
describe('useColorScheme', () => {

View File

@ -45,6 +45,7 @@ export const mockCurrentWorkspaceMembers: CurrentWorkspaceMember[] =
dateFormat,
timeFormat,
timeZone,
userEmail,
}) => ({
id,
locale,
@ -54,5 +55,6 @@ export const mockCurrentWorkspaceMembers: CurrentWorkspaceMember[] =
dateFormat,
timeFormat,
timeZone,
userEmail,
}),
);

View File

@ -63,7 +63,7 @@ export class WorkspaceMemberTranspiler {
} = workspaceMemberEntity;
const avatarUrl = this.generateSignedAvatarUrl({
workspaceId: userWorkspace.id,
workspaceId: userWorkspace.workspaceId,
workspaceMember: {
avatarUrl: avatarUrlFromEntity,
id,

View File

@ -293,7 +293,9 @@ const StyledButton = styled('button', {
}`
: 'none'};
color: ${!inverted
? theme.font.color.danger
? !disabled
? theme.font.color.danger
: theme.color.red20
: theme.font.color.inverted};
&:hover {
background: ${!inverted

View File

@ -1,19 +1,15 @@
import { useTheme } from '@emotion/react';
import {
Avatar,
AvatarProps,
IconChevronRight,
OverflowingTextWithTooltip,
} from '@ui/display';
import { Avatar, AvatarProps, IconChevronRight } from '@ui/display';
import { LightIconButtonGroup } from '@ui/input';
import { MenuItemIconButton } from '@ui/navigation/menu-item/components/MenuItem';
import { MouseEvent } from 'react';
import { MenuItemLeftContent } from '@ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { MouseEvent, ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
import { isDefined } from 'twenty-shared/utils';
export type MenuItemAvatarProps = {
accent?: MenuItemAccent;
@ -31,6 +27,7 @@ export type MenuItemAvatarProps = {
testId?: string;
text: string;
hasSubMenu?: boolean;
contextualText?: ReactNode;
};
// TODO: merge with MenuItem
@ -46,6 +43,7 @@ export const MenuItemAvatar = ({
avatar,
hasSubMenu = false,
text,
contextualText,
}: MenuItemAvatarProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
@ -69,16 +67,22 @@ export const MenuItemAvatar = ({
onMouseLeave={onMouseLeave}
>
<StyledMenuItemLeftContent>
{isDefined(avatar) && (
<Avatar
placeholder={avatar.placeholder}
avatarUrl={avatar.avatarUrl}
placeholderColorSeed={avatar.placeholderColorSeed}
size={avatar.size}
type={avatar.type}
/>
)}
<OverflowingTextWithTooltip text={text ?? ''} />
<MenuItemLeftContent
LeftIcon={undefined}
LeftComponent={
isDefined(avatar) ? (
<Avatar
placeholder={avatar.placeholder}
avatarUrl={avatar.avatarUrl}
placeholderColorSeed={avatar.placeholderColorSeed}
size={avatar.size}
type={avatar.type}
/>
) : undefined
}
text={text}
contextualText={contextualText}
/>
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
{showIconButtons && (