[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', lastName: 'Doe',
}, },
colorScheme: 'Light' as const, colorScheme: 'Light' as const,
userEmail: 'userEmail',
}; };
const mockWorkspace = { const mockWorkspace = {
@ -200,6 +201,7 @@ describe('ApolloFactory', () => {
lastName: 'Doe', lastName: 'Doe',
}, },
colorScheme: 'Light' as const, colorScheme: 'Light' as const,
userEmail: 'userEmail',
}; };
apolloFactory.updateWorkspaceMember(newWorkspaceMember); apolloFactory.updateWorkspaceMember(newWorkspaceMember);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -74,6 +74,7 @@ describe('useFilteredSearchRecordQuery', () => {
name: { firstName: 'John', lastName: 'Connor' }, name: { firstName: 'John', lastName: 'Connor' },
locale: 'en', locale: 'en',
colorScheme: 'Light', colorScheme: 'Light',
userEmail: 'userEmail',
}); });
const setMetadataItems = useSetRecoilState(objectMetadataItemsState); 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { ChangeEvent, useState } from 'react'; import { ChangeEvent, useState } from 'react';
@ -38,7 +39,7 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<DropdownContent> <DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuSearchInput <DropdownMenuSearchInput
value={searchFilter} value={searchFilter}
onChange={handleSearchFilterChange} onChange={handleSearchFilterChange}

View File

@ -39,20 +39,25 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
return ( return (
<> <>
{enrichedWorkspaceMembers.map((workspaceMember) => ( {enrichedWorkspaceMembers.map((workspaceMember) => {
<MenuItemAvatar const workspaceMemberFullName = `${workspaceMember?.name.firstName ?? ''} ${workspaceMember?.name.lastName ?? ''}`;
key={workspaceMember.id}
onClick={() => onSelect(workspaceMember)} return (
avatar={{ <MenuItemAvatar
type: 'rounded', key={workspaceMember.id}
size: 'md', onClick={() => onSelect(workspaceMember)}
placeholder: workspaceMember?.name.firstName ?? '', avatar={{
placeholderColorSeed: workspaceMember?.id, type: 'rounded',
avatarUrl: workspaceMember?.avatarUrl, size: 'md',
}} placeholder: workspaceMemberFullName,
text={workspaceMember?.name.firstName ?? ''} 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 { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
@ -77,7 +78,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<DropdownContent widthInPixels={320}> <DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader <DropdownMenuHeader
StartComponent={ StartComponent={
<DropdownMenuHeaderLeftComponent <DropdownMenuHeaderLeftComponent

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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