[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',
|
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);
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -1012,4 +1012,5 @@ export const mockWorkspaceMember = {
|
|||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
userId: '1',
|
userId: '1',
|
||||||
|
userEmail: 'userEmail',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -229,6 +229,7 @@ describe('buildValueFromFilter', () => {
|
|||||||
dateFormat: null,
|
dateFormat: null,
|
||||||
timeFormat: null,
|
timeFormat: null,
|
||||||
timeZone: null,
|
timeZone: null,
|
||||||
|
userEmail: 'userEmail',
|
||||||
};
|
};
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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', () => {
|
||||||
|
|||||||
@ -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,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user