diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 9b695a3f3c83..0ad27183ca46 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -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)}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx new file mode 100644 index 000000000000..4aeafd96f1fd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx @@ -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 ( + + + + + + + + {!isCompactModeActive && + skeletonItems.map(({ id }) => ( + + + + + + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 96d14a011bd4..79c786df1225 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -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 ( - + {isRecordIndexBoardColumnLoading ? ( + Array.from( + { + length: getNumberOfCardsPerColumnForSkeletonLoading( + columnDefinition.position, + ), + }, + (_, index) => ( + + + + ), + ) + ) : ( + + )} { + const skeletonCounts: Record = { + 0: 2, + 1: 1, + 2: 3, + 3: 0, + 4: 1, + }; + + return skeletonCounts[columnIndex] || 0; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx index 804df9806dd5..654254b86c07 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx @@ -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) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx index 640bfffee55b..e3a83b8348a0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx @@ -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 ; + } + return ( <> { + const { visibleTableColumnsSelector } = useRecordTableStates(); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); + + return ( + + {Array.from({ length: 8 }).map((_, rowIndex) => ( + + + + + + + + {visibleTableColumns.map((column) => ( + + ))} + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx index 5ec7e8ff4269..7fc7160e0b74 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx new file mode 100644 index 000000000000..12050e5b6740 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellLoading.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx new file mode 100644 index 000000000000..b010bdaecd6d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx @@ -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 ( + + + + ); +}; + +export const RecordTableCellSkeletonLoader = () => { + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/states/isRecordBoardColumnLoadingFamilyState.ts b/packages/twenty-front/src/modules/object-record/states/isRecordBoardColumnLoadingFamilyState.ts new file mode 100644 index 000000000000..8804ff588ba1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/states/isRecordBoardColumnLoadingFamilyState.ts @@ -0,0 +1,9 @@ +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; + +export const isRecordIndexBoardColumnLoadingFamilyState = createFamilyState< + boolean, + string | undefined +>({ + key: 'isRecordIndexBoardColumnLoadingFamilyState', + defaultValue: false, +});