Data Skeleton Loading on Indexes (#5828)

### Description
Data Skeleton Loading on Indexes

### Refs
#4459

### Demo


https://github.com/twentyhq/twenty/assets/140154534/d9c9b0fa-2d8c-4b0d-8d48-cae09530622a


Fixes #4459

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Matheus <matheus_benini@hotmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
gitstart-twenty
2024-06-19 09:25:20 -04:00
committed by GitHub
parent ff21396bc6
commit 701059007b
11 changed files with 240 additions and 8 deletions

View File

@ -66,7 +66,7 @@ const StyledBoardCardWrapper = styled.div`
width: 100%;
`;
const StyledBoardCardHeader = styled.div<{
export const StyledBoardCardHeader = styled.div<{
showCompactView: boolean;
}>`
align-items: center;
@ -89,7 +89,7 @@ const StyledBoardCardHeader = styled.div<{
}
`;
const StyledBoardCardBody = styled.div`
export const StyledBoardCardBody = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};

View File

@ -0,0 +1,61 @@
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
StyledBoardCardBody,
StyledBoardCardHeader,
} from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
const StyledSkeletonIconAndText = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledSkeletonTitle = styled.div`
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledSeparator = styled.div`
height: ${({ theme }) => theme.spacing(2)};
`;
export const RecordBoardColumnCardContainerSkeletonLoader = ({
numberOfFields,
titleSkeletonWidth,
isCompactModeActive,
}: {
numberOfFields: number;
titleSkeletonWidth: number;
isCompactModeActive: boolean;
}) => {
const theme = useTheme();
const skeletonItems = Array.from({ length: numberOfFields }).map(
(_, index) => ({
id: `skeleton-item-${index}`,
}),
);
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
<StyledSkeletonTitle>
<Skeleton width={titleSkeletonWidth} height={16} />
</StyledSkeletonTitle>
</StyledBoardCardHeader>
<StyledSeparator />
{!isCompactModeActive &&
skeletonItems.map(({ id }) => (
<StyledBoardCardBody key={id}>
<StyledSkeletonIconAndText>
<Skeleton width={16} height={16} />
<Skeleton width={151} height={16} />
</StyledSkeletonIconAndText>
</StyledBoardCardBody>
))}
</SkeletonTheme>
);
};

View File

@ -1,14 +1,19 @@
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader';
import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader';
import { RecordBoardColumnNewButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton';
import { RecordBoardColumnNewOpportunityButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading';
import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState';
const StyledColumnCardsContainer = styled.div`
display: flex;
@ -20,6 +25,17 @@ const StyledNewButtonContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledSkeletonCardContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.background.quaternary};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow:
0px 4px 8px 0px rgba(0, 0, 0, 0.08),
0px 0px 4px 0px rgba(0, 0, 0, 0.08);
color: ${({ theme }) => theme.font.color.primary};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
type RecordBoardColumnCardsContainerProps = {
recordIds: string[];
droppableProvided: DroppableProvided;
@ -32,13 +48,51 @@ export const RecordBoardColumnCardsContainer = ({
const { columnDefinition } = useContext(RecordBoardColumnContext);
const { objectMetadataItem } = useContext(RecordBoardContext);
const columnId = columnDefinition.id;
const isRecordIndexBoardColumnLoading = useRecoilValue(
isRecordIndexBoardColumnLoadingFamilyState(columnId),
);
const { isCompactModeActiveState, visibleFieldDefinitionsState } =
useRecordBoardStates();
const visibleFieldDefinitions = useRecoilValue(
visibleFieldDefinitionsState(),
);
const numberOfFields = visibleFieldDefinitions.length;
const isCompactModeActive = useRecoilValue(isCompactModeActiveState);
return (
<StyledColumnCardsContainer
ref={droppableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided?.droppableProps}
>
<RecordBoardColumnCardsMemo recordIds={recordIds} />
{isRecordIndexBoardColumnLoading ? (
Array.from(
{
length: getNumberOfCardsPerColumnForSkeletonLoading(
columnDefinition.position,
),
},
(_, index) => (
<StyledSkeletonCardContainer
key={`${columnDefinition.id}-${index}`}
>
<RecordBoardColumnCardContainerSkeletonLoader
numberOfFields={numberOfFields}
titleSkeletonWidth={isCompactModeActive ? 72 : 54}
isCompactModeActive={isCompactModeActive}
/>
</StyledSkeletonCardContainer>
),
)
) : (
<RecordBoardColumnCardsMemo recordIds={recordIds} />
)}
<RecordBoardColumnFetchMoreLoader />
<Draggable
draggableId={`new-${columnDefinition.id}`}

View File

@ -0,0 +1,13 @@
export const getNumberOfCardsPerColumnForSkeletonLoading = (
columnIndex: number,
): number => {
const skeletonCounts: Record<number, number> = {
0: 2,
1: 1,
2: 3,
3: 0,
4: 1,
};
return skeletonCounts[columnIndex] || 0;
};

View File

@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn';
import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
export const RecordIndexBoardColumnLoaderEffect = ({
@ -34,7 +35,7 @@ export const RecordIndexBoardColumnLoaderEffect = ({
}),
);
const { fetchMoreRecords, loading, hasNextPage } =
const { fetchMoreRecords, loading, records, hasNextPage } =
useLoadRecordIndexBoardColumn({
objectNameSingular,
recordBoardId,
@ -43,6 +44,14 @@ export const RecordIndexBoardColumnLoaderEffect = ({
columnId,
});
const setIsRecordIndexLoading = useSetRecoilState(
isRecordIndexBoardColumnLoadingFamilyState(columnId),
);
useEffect(() => {
setIsRecordIndexLoading(loading && records.length === 0);
}, [records, loading, setIsRecordIndexLoading]);
useEffect(() => {
const run = async () => {
if (!loading && shouldFetchMore && hasNextPage) {

View File

@ -1,6 +1,7 @@
import { useRecoilValue } from 'recoil';
import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/components/RecordTableBodyFetchMoreLoader';
import { RecordTableBodyLoading } from '@/object-record/record-table/components/RecordTableBodyLoading';
import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { DraggableTableBody } from '@/ui/layout/draggable-list/components/DraggableTableBody';
@ -14,10 +15,19 @@ export const RecordTableBody = ({
objectNameSingular,
recordTableId,
}: RecordTableBodyProps) => {
const { tableRowIdsState } = useRecordTableStates();
const { tableRowIdsState, isRecordTableInitialLoadingState } =
useRecordTableStates();
const tableRowIds = useRecoilValue(tableRowIdsState);
const isRecordTableInitialLoading = useRecoilValue(
isRecordTableInitialLoadingState,
);
if (isRecordTableInitialLoading && tableRowIds.length === 0) {
return <RecordTableBodyLoading />;
}
return (
<>
<DraggableTableBody

View File

@ -0,0 +1,37 @@
import { useRecoilValue } from 'recoil';
import { CheckboxCell } from '@/object-record/record-table/components/CheckboxCell';
import { GripCell } from '@/object-record/record-table/components/GripCell';
import {
StyledTd,
StyledTr,
} from '@/object-record/record-table/components/RecordTableRow';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
export const RecordTableBodyLoading = () => {
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
return (
<tbody>
{Array.from({ length: 8 }).map((_, rowIndex) => (
<StyledTr
isDragging={false}
data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`}
>
<StyledTd data-select-disable>
<GripCell isDragging={false} />
</StyledTd>
<StyledTd>
<CheckboxCell />
</StyledTd>
{visibleTableColumns.map((column) => (
<RecordTableCellLoading key={column.fieldMetadataId} />
))}
</StyledTr>
))}
</tbody>
);
};

View File

@ -23,12 +23,12 @@ type RecordTableRowProps = {
isPendingRow?: boolean;
};
const StyledTd = styled.td`
export const StyledTd = styled.td`
position: relative;
user-select: none;
`;
const StyledTr = styled.tr<{ isDragging: boolean }>`
export const StyledTr = styled.tr<{ isDragging: boolean }>`
border: 1px solid transparent;
transition: border-left-color 0.2s ease-in-out;

View File

@ -0,0 +1,10 @@
import { StyledTd } from '@/object-record/record-table/components/RecordTableRow';
import { RecordTableCellSkeletonLoader } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader';
export const RecordTableCellLoading = () => {
return (
<StyledTd>
<RecordTableCellSkeletonLoader />
</StyledTd>
);
};

View File

@ -0,0 +1,29 @@
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
const StyledSkeletonContainer = styled.div`
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledRecordTableCellLoader = ({ width }: { width?: number }) => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton width={width} height={16} />
</SkeletonTheme>
);
};
export const RecordTableCellSkeletonLoader = () => {
return (
<StyledSkeletonContainer>
<StyledRecordTableCellLoader />
</StyledSkeletonContainer>
);
};

View File

@ -0,0 +1,9 @@
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export const isRecordIndexBoardColumnLoadingFamilyState = createFamilyState<
boolean,
string | undefined
>({
key: 'isRecordIndexBoardColumnLoadingFamilyState',
defaultValue: false,
});