Add empty states to settings tables (#10978)

## Context
Fixes https://github.com/twentyhq/twenty/issues/10964

## Test
<img width="617" alt="Screenshot 2025-03-18 at 12 18 30"
src="https://github.com/user-attachments/assets/dab8738d-d221-4a6b-a72e-061ab5fffb70"
/>
<img width="647" alt="Screenshot 2025-03-18 at 12 18 25"
src="https://github.com/user-attachments/assets/45466a80-7a80-4cde-a0c5-420cd6c05cb2"
/>
<img width="637" alt="Screenshot 2025-03-18 at 12 18 19"
src="https://github.com/user-attachments/assets/46a9f27a-bd3a-4e91-9885-668cf780d562"
/>
<img width="630" alt="Screenshot 2025-03-18 at 12 18 07"
src="https://github.com/user-attachments/assets/e1f805a0-ed7f-4cf2-8f75-78b865bd1ca2"
/>
<img width="649" alt="Screenshot 2025-03-18 at 12 18 01"
src="https://github.com/user-attachments/assets/e9f3086f-fe97-4f3b-99e0-25249e9dd43b"
/>
This commit is contained in:
Weiko
2025-03-18 14:18:33 +01:00
committed by GitHub
parent 1ca5a5e9f6
commit be1b877868
6 changed files with 210 additions and 160 deletions

View File

@ -7,6 +7,7 @@ import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useState } from 'react'; import { useState } from 'react';
@ -41,19 +42,14 @@ const StyledSearchContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)}; padding-bottom: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledTable = styled.div<{ hasRows: boolean }>` const StyledTable = styled.div`
border-bottom: ${({ hasRows, theme }) => border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
hasRows ? `1px solid ${theme.border.color.light}` : 'none'};
`; `;
const StyledSearchInput = styled(TextInput)` const StyledSearchInput = styled(TextInput)`
input { input {
background: ${({ theme }) => theme.background.transparent.lighter}; background: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
&:hover {
border: 1px solid ${({ theme }) => theme.border.color.medium};
}
} }
`; `;
@ -63,6 +59,10 @@ const StyledTableRows = styled.div`
padding-top: ${({ theme }) => theme.spacing(2)}; padding-top: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledNoMembers = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
type RoleAssignmentProps = { type RoleAssignmentProps = {
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & { role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & {
workspaceMembers: Array<WorkspaceMember>; workspaceMembers: Array<WorkspaceMember>;
@ -175,21 +175,29 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
<StyledSearchInput <StyledSearchInput
value={searchFilter} value={searchFilter}
onChange={handleSearchChange} onChange={handleSearchChange}
placeholder={t`Search a member`} placeholder={t`Search an assigned team member...`}
fullWidth fullWidth
LeftIcon={IconSearch} LeftIcon={IconSearch}
sizeVariant="lg" sizeVariant="lg"
/> />
</StyledSearchContainer> </StyledSearchContainer>
<StyledTable hasRows={filteredWorkspaceMembers.length > 0}> <StyledTable>
<RoleAssignmentTableHeader /> <RoleAssignmentTableHeader />
<StyledTableRows> <StyledTableRows>
{filteredWorkspaceMembers.map((workspaceMember) => ( {filteredWorkspaceMembers.length > 0 ? (
<RoleAssignmentTableRow filteredWorkspaceMembers.map((workspaceMember) => (
key={workspaceMember.id} <RoleAssignmentTableRow
workspaceMember={workspaceMember} key={workspaceMember.id}
/> workspaceMember={workspaceMember}
))} />
))
) : (
<StyledNoMembers>
{!searchFilter
? t`No members assigned`
: t`No members match your search`}
</StyledNoMembers>
)}
</StyledTableRows> </StyledTableRows>
</StyledTable> </StyledTable>

View File

@ -1,13 +1,10 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
export const RoleAssignmentTableHeader = () => ( export const RoleAssignmentTableHeader = () => (
<Table> <TableRow gridAutoColumns="2fr 4fr">
<TableRow gridAutoColumns="2fr 4fr"> <TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Name`}</TableHeader> <TableHeader>{t`Email`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader> </TableRow>
</TableRow>
</Table>
); );

View File

@ -32,7 +32,11 @@ const StyledRolePermissionsContainer = styled.div`
const StyledTable = styled.div` const StyledTable = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)}; padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`; `;
type RolePermissionsProps = { type RolePermissionsProps = {
@ -136,12 +140,14 @@ export const RolePermissions = ({ role }: RolePermissionsProps) => {
/> />
<StyledTable> <StyledTable>
<RolePermissionsObjectsTableHeader allPermissions={true} /> <RolePermissionsObjectsTableHeader allPermissions={true} />
{objectPermissionsConfig.map((permission) => ( <StyledTableRows>
<RolePermissionsObjectsTableRow {objectPermissionsConfig.map((permission) => (
key={permission.key} <RolePermissionsObjectsTableRow
permission={permission} key={permission.key}
/> permission={permission}
))} />
))}
</StyledTableRows>
</StyledTable> </StyledTable>
</Section> </Section>
<Section> <Section>
@ -150,12 +156,14 @@ export const RolePermissions = ({ role }: RolePermissionsProps) => {
<RolePermissionsSettingsTableHeader <RolePermissionsSettingsTableHeader
allPermissions={role.canUpdateAllSettings} allPermissions={role.canUpdateAllSettings}
/> />
{settingsPermissionsConfig.map((permission) => ( <StyledTableRows>
<RolePermissionsSettingsTableRow {settingsPermissionsConfig.map((permission) => (
key={permission.key} <RolePermissionsSettingsTableRow
permission={permission} key={permission.key}
/> permission={permission}
))} />
))}
</StyledTableRows>
</StyledTable> </StyledTable>
</Section> </Section>
</StyledRolePermissionsContainer> </StyledRolePermissionsContainer>

View File

@ -1,4 +1,3 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -20,25 +19,17 @@ const StyledActionsHeader = styled(TableHeader)`
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(4)};
`; `;
const StyledTable = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
type RolePermissionsObjectsTableHeaderProps = { type RolePermissionsObjectsTableHeaderProps = {
className?: string;
allPermissions: boolean; allPermissions: boolean;
}; };
export const RolePermissionsObjectsTableHeader = ({ export const RolePermissionsObjectsTableHeader = ({
className,
allPermissions, allPermissions,
}: RolePermissionsObjectsTableHeaderProps) => ( }: RolePermissionsObjectsTableHeaderProps) => (
<StyledTable className={className}> <StyledTableHeaderRow>
<StyledTableHeaderRow> <StyledNameHeader>{t`Name`}</StyledNameHeader>
<StyledNameHeader>{t`Name`}</StyledNameHeader> <StyledActionsHeader aria-label={t`Actions`}>
<StyledActionsHeader aria-label={t`Actions`}> <Checkbox checked={allPermissions} disabled />
<Checkbox checked={allPermissions} disabled /> </StyledActionsHeader>
</StyledActionsHeader> </StyledTableHeaderRow>
</StyledTableHeaderRow>
</StyledTable>
); );

View File

@ -1,4 +1,3 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -23,30 +22,22 @@ const StyledActionsHeader = styled(TableHeader)`
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(4)};
`; `;
const StyledTable = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTypeHeader = styled(TableHeader)` const StyledTypeHeader = styled(TableHeader)`
flex: 1; flex: 1;
`; `;
type RolePermissionsSettingsTableHeaderProps = { type RolePermissionsSettingsTableHeaderProps = {
className?: string;
allPermissions: boolean; allPermissions: boolean;
}; };
export const RolePermissionsSettingsTableHeader = ({ export const RolePermissionsSettingsTableHeader = ({
className,
allPermissions, allPermissions,
}: RolePermissionsSettingsTableHeaderProps) => ( }: RolePermissionsSettingsTableHeaderProps) => (
<StyledTable className={className}> <StyledTableHeaderRow>
<StyledTableHeaderRow> <StyledNameHeader>{t`Name`}</StyledNameHeader>
<StyledNameHeader>{t`Name`}</StyledNameHeader> <StyledTypeHeader>{t`Type`}</StyledTypeHeader>
<StyledTypeHeader>{t`Type`}</StyledTypeHeader> <StyledActionsHeader aria-label={t`Actions`}>
<StyledActionsHeader aria-label={t`Actions`}> <Checkbox checked={allPermissions} disabled />
<Checkbox checked={allPermissions} disabled /> </StyledActionsHeader>
</StyledActionsHeader> </StyledTableHeaderRow>
</StyledTableHeaderRow>
</StyledTable>
); );

View File

@ -11,6 +11,7 @@ import {
IconButton, IconButton,
IconMail, IconMail,
IconReload, IconReload,
IconSearch,
IconTrash, IconTrash,
Section, Section,
Status, Status,
@ -26,6 +27,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Table } from '@/ui/layout/table/components/Table'; import { Table } from '@/ui/layout/table/components/Table';
@ -51,11 +53,7 @@ const StyledButtonContainer = styled.div`
`; `;
const StyledTable = styled(Table)` const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
`; `;
const StyledIconWrapper = styled.div` const StyledIconWrapper = styled.div`
@ -70,6 +68,26 @@ const StyledTextContainerWithEllipsis = styled.div`
white-space: nowrap; white-space: nowrap;
`; `;
const StyledSearchContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledSearchInput = styled(TextInput)`
input {
background: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
}
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledNoMembers = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsWorkspaceMembers = () => { export const SettingsWorkspaceMembers = () => {
const { t } = useLingui(); const { t } = useLingui();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -100,6 +118,12 @@ export const SettingsWorkspaceMembers = () => {
const workspaceInvitations = useRecoilValue(workspaceInvitationsState); const workspaceInvitations = useRecoilValue(workspaceInvitationsState);
const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState); const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState);
const [searchFilter, setSearchFilter] = useState('');
const handleSearchChange = (text: string) => {
setSearchFilter(text);
};
useGetWorkspaceInvitationsQuery({ useGetWorkspaceInvitationsQuery({
onError: (error: Error) => { onError: (error: Error) => {
enqueueSnackBar(error.message, { enqueueSnackBar(error.message, {
@ -138,6 +162,21 @@ export const SettingsWorkspaceMembers = () => {
: formatDistanceToNow(new Date(expiresAt)); : formatDistanceToNow(new Date(expiresAt));
}; };
const filteredWorkspaceMembers = !searchFilter
? workspaceMembers
: workspaceMembers.filter((member) => {
const searchTerm = searchFilter.toLowerCase();
const firstName = member.name.firstName?.toLowerCase() || '';
const lastName = member.name.lastName?.toLowerCase() || '';
const email = member.userEmail?.toLowerCase() || '';
return (
firstName.includes(searchTerm) ||
lastName.includes(searchTerm) ||
email.includes(searchTerm)
);
});
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title={t`Members`} title={t`Members`}
@ -167,77 +206,94 @@ export const SettingsWorkspaceMembers = () => {
title={t`Manage Members`} title={t`Manage Members`}
description={t`Manage the members of your space here`} description={t`Manage the members of your space here`}
/> />
<Table> <StyledSearchContainer>
<StyledTableHeaderRow> <StyledSearchInput
<TableRow value={searchFilter}
gridAutoColumns="150px 1fr 1fr" onChange={handleSearchChange}
mobileGridAutoColumns="100px 1fr 1fr" placeholder={t`Search a team member...`}
> fullWidth
<TableHeader> LeftIcon={IconSearch}
<Trans>Name</Trans> sizeVariant="lg"
</TableHeader> />
<TableHeader> </StyledSearchContainer>
<Trans>Email</Trans> <StyledTable>
</TableHeader> <TableRow
<TableHeader align={'right'}></TableHeader> gridAutoColumns="150px 1fr 1fr"
</TableRow> mobileGridAutoColumns="100px 1fr 1fr"
</StyledTableHeaderRow> >
{workspaceMembers?.map((workspaceMember) => ( <TableHeader>
<StyledTable key={workspaceMember.id}> <Trans>Name</Trans>
<TableRow </TableHeader>
gridAutoColumns="150px 1fr 1fr" <TableHeader>
mobileGridAutoColumns="100px 1fr 1fr" <Trans>Email</Trans>
> </TableHeader>
<TableCell> <TableHeader align={'right'}></TableHeader>
<StyledIconWrapper> </TableRow>
<Avatar <StyledTableRows>
avatarUrl={workspaceMember.avatarUrl} {filteredWorkspaceMembers.length > 0 ? (
placeholderColorSeed={workspaceMember.id} filteredWorkspaceMembers.map((workspaceMember) => (
placeholder={workspaceMember.name.firstName ?? ''} <TableRow
type="rounded" gridAutoColumns="150px 1fr 1fr"
size="sm" mobileGridAutoColumns="100px 1fr 1fr"
/> key={workspaceMember.id}
</StyledIconWrapper> >
<StyledTextContainerWithEllipsis <TableCell>
id={`hover-text-${workspaceMember.id}`} <StyledIconWrapper>
> <Avatar
{workspaceMember.name.firstName + avatarUrl={workspaceMember.avatarUrl}
' ' + placeholderColorSeed={workspaceMember.id}
workspaceMember.name.lastName} placeholder={workspaceMember.name.firstName ?? ''}
</StyledTextContainerWithEllipsis> type="rounded"
<AppTooltip size="sm"
anchorSelect={`#hover-text-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</TableCell>
<TableCell>
<StyledTextContainerWithEllipsis>
{workspaceMember.userEmail}
</StyledTextContainerWithEllipsis>
</TableCell>
<TableCell align={'right'}>
{currentWorkspaceMember?.id !== workspaceMember.id && (
<StyledButtonContainer>
<IconButton
onClick={() => {
setIsConfirmationModalOpen(true);
setWorkspaceMemberToDelete(workspaceMember.id);
}}
variant="tertiary"
size="medium"
Icon={IconTrash}
/> />
</StyledButtonContainer> </StyledIconWrapper>
)} <StyledTextContainerWithEllipsis
</TableCell> id={`hover-text-${workspaceMember.id}`}
</TableRow> >
</StyledTable> {workspaceMember.name.firstName +
))} ' ' +
</Table> workspaceMember.name.lastName}
</StyledTextContainerWithEllipsis>
<AppTooltip
anchorSelect={`#hover-text-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</TableCell>
<TableCell>
<StyledTextContainerWithEllipsis>
{workspaceMember.userEmail}
</StyledTextContainerWithEllipsis>
</TableCell>
<TableCell align={'right'}>
{currentWorkspaceMember?.id !== workspaceMember.id && (
<StyledButtonContainer>
<IconButton
onClick={() => {
setIsConfirmationModalOpen(true);
setWorkspaceMemberToDelete(workspaceMember.id);
}}
variant="tertiary"
size="medium"
Icon={IconTrash}
/>
</StyledButtonContainer>
)}
</TableCell>
</TableRow>
))
) : (
<StyledNoMembers>
{!searchFilter
? t`No members`
: t`No members match your search`}
</StyledNoMembers>
)}
</StyledTableRows>
</StyledTable>
</Section> </Section>
<Section> <Section>
<H2Title <H2Title
@ -246,26 +302,25 @@ export const SettingsWorkspaceMembers = () => {
/> />
<WorkspaceInviteTeam /> <WorkspaceInviteTeam />
{isNonEmptyArray(workspaceInvitations) && ( {isNonEmptyArray(workspaceInvitations) && (
<Table> <StyledTable>
<StyledTableHeaderRow> <TableRow
<TableRow gridAutoColumns="150px 1fr 1fr"
gridAutoColumns="150px 1fr 1fr" mobileGridAutoColumns="100px 1fr 1fr"
mobileGridAutoColumns="100px 1fr 1fr" >
> <TableHeader>
<TableHeader> <Trans>Email</Trans>
<Trans>Email</Trans> </TableHeader>
</TableHeader> <TableHeader align={'right'}>
<TableHeader align={'right'}> <Trans>Expires in</Trans>
<Trans>Expires in</Trans> </TableHeader>
</TableHeader> <TableHeader></TableHeader>
<TableHeader></TableHeader> </TableRow>
</TableRow> <StyledTableRows>
</StyledTableHeaderRow> {workspaceInvitations?.map((workspaceInvitation) => (
{workspaceInvitations?.map((workspaceInvitation) => (
<StyledTable key={workspaceInvitation.id}>
<TableRow <TableRow
gridAutoColumns="150px 1fr 1fr" gridAutoColumns="150px 1fr 1fr"
mobileGridAutoColumns="100px 1fr 1fr" mobileGridAutoColumns="100px 1fr 1fr"
key={workspaceInvitation.id}
> >
<TableCell> <TableCell>
<StyledIconWrapper> <StyledIconWrapper>
@ -309,9 +364,9 @@ export const SettingsWorkspaceMembers = () => {
</StyledButtonContainer> </StyledButtonContainer>
</TableCell> </TableCell>
</TableRow> </TableRow>
</StyledTable> ))}
))} </StyledTableRows>
</Table> </StyledTable>
)} )}
</Section> </Section>
</SettingsPageContainer> </SettingsPageContainer>