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:
@ -66,7 +66,7 @@ const StyledBoardCardWrapper = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledBoardCardHeader = styled.div<{
|
export const StyledBoardCardHeader = styled.div<{
|
||||||
showCompactView: boolean;
|
showCompactView: boolean;
|
||||||
}>`
|
}>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -89,7 +89,7 @@ const StyledBoardCardHeader = styled.div<{
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledBoardCardBody = styled.div`
|
export const StyledBoardCardBody = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,14 +1,19 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
|
import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
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 { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
|
||||||
import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader';
|
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 { RecordBoardColumnNewButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton';
|
||||||
import { RecordBoardColumnNewOpportunityButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton';
|
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 { 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`
|
const StyledColumnCardsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -20,6 +25,17 @@ const StyledNewButtonContainer = styled.div`
|
|||||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
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 = {
|
type RecordBoardColumnCardsContainerProps = {
|
||||||
recordIds: string[];
|
recordIds: string[];
|
||||||
droppableProvided: DroppableProvided;
|
droppableProvided: DroppableProvided;
|
||||||
@ -32,13 +48,51 @@ export const RecordBoardColumnCardsContainer = ({
|
|||||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||||
const { objectMetadataItem } = useContext(RecordBoardContext);
|
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 (
|
return (
|
||||||
<StyledColumnCardsContainer
|
<StyledColumnCardsContainer
|
||||||
ref={droppableProvided?.innerRef}
|
ref={droppableProvided?.innerRef}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...droppableProvided?.droppableProps}
|
{...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 />
|
<RecordBoardColumnFetchMoreLoader />
|
||||||
<Draggable
|
<Draggable
|
||||||
draggableId={`new-${columnDefinition.id}`}
|
draggableId={`new-${columnDefinition.id}`}
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||||
import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn';
|
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';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
export const RecordIndexBoardColumnLoaderEffect = ({
|
export const RecordIndexBoardColumnLoaderEffect = ({
|
||||||
@ -34,7 +35,7 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { fetchMoreRecords, loading, hasNextPage } =
|
const { fetchMoreRecords, loading, records, hasNextPage } =
|
||||||
useLoadRecordIndexBoardColumn({
|
useLoadRecordIndexBoardColumn({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
@ -43,6 +44,14 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
|||||||
columnId,
|
columnId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setIsRecordIndexLoading = useSetRecoilState(
|
||||||
|
isRecordIndexBoardColumnLoadingFamilyState(columnId),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsRecordIndexLoading(loading && records.length === 0);
|
||||||
|
}, [records, loading, setIsRecordIndexLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (!loading && shouldFetchMore && hasNextPage) {
|
if (!loading && shouldFetchMore && hasNextPage) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/components/RecordTableBodyFetchMoreLoader';
|
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 { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { DraggableTableBody } from '@/ui/layout/draggable-list/components/DraggableTableBody';
|
import { DraggableTableBody } from '@/ui/layout/draggable-list/components/DraggableTableBody';
|
||||||
@ -14,10 +15,19 @@ export const RecordTableBody = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordTableId,
|
recordTableId,
|
||||||
}: RecordTableBodyProps) => {
|
}: RecordTableBodyProps) => {
|
||||||
const { tableRowIdsState } = useRecordTableStates();
|
const { tableRowIdsState, isRecordTableInitialLoadingState } =
|
||||||
|
useRecordTableStates();
|
||||||
|
|
||||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
|
const isRecordTableInitialLoading = useRecoilValue(
|
||||||
|
isRecordTableInitialLoadingState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRecordTableInitialLoading && tableRowIds.length === 0) {
|
||||||
|
return <RecordTableBodyLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DraggableTableBody
|
<DraggableTableBody
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -23,12 +23,12 @@ type RecordTableRowProps = {
|
|||||||
isPendingRow?: boolean;
|
isPendingRow?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledTd = styled.td`
|
export const StyledTd = styled.td`
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTr = styled.tr<{ isDragging: boolean }>`
|
export const StyledTr = styled.tr<{ isDragging: boolean }>`
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: border-left-color 0.2s ease-in-out;
|
transition: border-left-color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
|
export const isRecordIndexBoardColumnLoadingFamilyState = createFamilyState<
|
||||||
|
boolean,
|
||||||
|
string | undefined
|
||||||
|
>({
|
||||||
|
key: 'isRecordIndexBoardColumnLoadingFamilyState',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user