Skip to content

Commit

Permalink
Build empty state for remote tables (#5652)
Browse files Browse the repository at this point in the history
Remote tables could be in an empty state because:
- either we do not have data, which is normal
- either the connexion is broken (issue with the server, table requires
updates...)

Apollo throws errors but these will quickly disappear and do not provide
any tips to the user on how handle those.

This PR adds a new empty state placeholder for remote objects, that will
be display when the record list is empty. It will provide a link to the
settings page.

<img width="1512" alt="Capture d’écran 2024-05-30 à 11 49 33"
src="https://github.com/twentyhq/twenty/assets/22936103/fc2dd3cc-e90b-4033-b023-83ac9ff2a70b">
  • Loading branch information
thomtrp authored and Weiko committed May 31, 2024
1 parent ab3aaae commit edf5120
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
orderBy,
limit,
onCompleted,
onError,
skip,
recordGqlFields,
fetchPolicy,
Expand All @@ -45,6 +46,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
totalCount?: number;
},
) => void;
onError?: (error?: Error) => void;
skip?: boolean;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
fetchPolicy?: WatchQueryFetchPolicy;
Expand Down Expand Up @@ -120,6 +122,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
variant: SnackBarVariant.Error,
},
);
onError?.(error);
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => {
setLastRowVisible(false);
setIsRecordTableInitialLoading(false);
},
onError: () => {
setIsRecordTableInitialLoading(false);
},
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useNavigate } from 'react-router-dom';
import { IconPlus, IconSettings } from 'twenty-ui';

import { Button } from '@/ui/input/button/components/Button';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';

type RecordTableEmptyStateProps = {
objectLabel: string;
createRecord: () => void;
isRemote: boolean;
};

export const RecordTableEmptyState = ({
objectLabel,
createRecord,
isRemote,
}: RecordTableEmptyStateProps) => {
const navigate = useNavigate();

const [title, subTitle, Icon, onClick, buttonTitle] = isRemote
? [
'No Data Available for Remote Table',
'If this is unexpected, please verify your settings.',
IconSettings,
() => navigate('/settings/integrations'),
'Go to Settings',
]
: [
`Add your first ${objectLabel}`,
`Use our API or add your first ${objectLabel} manually`,
IconPlus,
createRecord,
`Add a ${objectLabel}`,
];

return (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={Icon}
title={buttonTitle}
variant={'secondary'}
onClick={onClick}
/>
</AnimatedPlaceholderEmptyContainer>
);
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui';

import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
import { EntityDeleteContext } from '@/object-record/record-table/contexts/EntityDeleteHookContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Button } from '@/ui/input/button/components/Button';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
Expand Down Expand Up @@ -56,15 +48,15 @@ export const RecordTableWithWrappers = ({
}: RecordTableWithWrappersProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);

const { numberOfTableRowsState, isRecordTableInitialLoadingState } =
const { isRecordTableInitialLoadingState, tableRowIdsState } =
useRecordTableStates(recordTableId);

const numberOfTableRows = useRecoilValue(numberOfTableRowsState);

const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);

const tableRowIds = useRecoilValue(tableRowIdsState);

const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,
});
Expand Down Expand Up @@ -116,25 +108,13 @@ export const RecordTableWithWrappers = ({
tableBodyRef={tableBodyRef}
/>
{!isRecordTableInitialLoading &&
numberOfTableRows === 0 &&
!isRemote && (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
Add your first {objectLabel}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
Use our API or add your first {objectLabel} manually
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={IconPlus}
title={`Add a ${objectLabel}`}
variant={'secondary'}
onClick={createRecord}
/>
</AnimatedPlaceholderEmptyContainer>
// we cannot rely on count states because this is not available for remote objects
tableRowIds.length === 0 && (
<RecordTableEmptyState
objectLabel={objectLabel}
createRecord={createRecord}
isRemote={isRemote}
/>
)}
</StyledTableContainer>
</StyledTableWithHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';

import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';

const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyState',
component: RecordTableEmptyState,
decorators: [MemoryRouterDecorator],
};

export default meta;
type Story = StoryObj<typeof RecordTableEmptyState>;

export const Default: Story = {
args: {
objectLabel: 'person',
isRemote: false,
createRecord: () => {},
},
};

export const Remote: Story = {
args: {
objectLabel: 'remote person',
isRemote: true,
createRecord: () => {},
},
};

0 comments on commit edf5120

Please sign in to comment.