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,
+});