[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:
@ -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);
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -1012,4 +1012,5 @@ export const mockWorkspaceMember = {
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
userId: '1',
|
||||
userEmail: 'userEmail',
|
||||
};
|
||||
|
||||
@ -43,6 +43,7 @@ describe('useFindManyRecords', () => {
|
||||
name: { firstName: 'John', lastName: 'Connor' },
|
||||
locale: 'en',
|
||||
colorScheme: 'Light',
|
||||
userEmail: 'userEmail',
|
||||
});
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -229,6 +229,7 @@ describe('buildValueFromFilter', () => {
|
||||
dateFormat: null,
|
||||
timeFormat: null,
|
||||
timeZone: null,
|
||||
userEmail: 'userEmail',
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
|
||||
@ -74,6 +74,7 @@ describe('useFilteredSearchRecordQuery', () => {
|
||||
name: { firstName: 'John', lastName: 'Connor' },
|
||||
locale: 'en',
|
||||
colorScheme: 'Light',
|
||||
userEmail: 'userEmail',
|
||||
});
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -63,7 +63,7 @@ export class WorkspaceMemberTranspiler {
|
||||
} = workspaceMemberEntity;
|
||||
|
||||
const avatarUrl = this.generateSignedAvatarUrl({
|
||||
workspaceId: userWorkspace.id,
|
||||
workspaceId: userWorkspace.workspaceId,
|
||||
workspaceMember: {
|
||||
avatarUrl: avatarUrlFromEntity,
|
||||
id,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user