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%;
|
||||
`;
|
||||
|
||||
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)};
|
||||
|
||||
@ -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 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}`}
|
||||
|
||||
@ -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 { 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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@ -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