diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
index 64f713e4d172..fe248f8b4e5d 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
@@ -1,3 +1,5 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@@ -27,15 +29,84 @@ const StyledMainContainer = styled.div`
justify-content: center;
`;
+const StyledSkeletonContainer = styled.div`
+ align-items: center;
+ width: 100%;
+ padding: ${({ theme }) => theme.spacing(8)};
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(4)};
+ flex-wrap: wrap;
+ align-content: flex-start;
+`;
+
+const StyledSkeletonSubSection = styled.div`
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(4)};
+`;
+
+const StyledSkeletonColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(3)};
+ justify-content: center;
+`;
+
+const StyledSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+
+ );
+};
+
+const StyledTimelineSkeletonLoader = () => {
+ const theme = useTheme();
+ const skeletonItems = Array.from({ length: 3 }).map((_, index) => ({
+ id: `skeleton-item-${index}`,
+ }));
+ return (
+
+
+
+ {skeletonItems.map(({ id }) => (
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
export const Timeline = ({
targetableObject,
+ loading,
}: {
targetableObject: ActivityTargetableObject;
+ loading?: boolean;
}) => {
const timelineActivitiesForGroup = useRecoilValue(
timelineActivitiesForGroupState,
);
+ if (loading === true) {
+ return ;
+ }
+
if (timelineActivitiesForGroup.length === 0) {
return (
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx
index a35a3b7cbe2e..7527c0eba63d 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx
@@ -17,9 +17,13 @@ import { RecordInlineCellContainer } from './RecordInlineCellContainer';
type RecordInlineCellProps = {
readonly?: boolean;
+ loading?: boolean;
};
-export const RecordInlineCell = ({ readonly }: RecordInlineCellProps) => {
+export const RecordInlineCell = ({
+ readonly,
+ loading,
+}: RecordInlineCellProps) => {
const { fieldDefinition, entityId } = useContext(FieldContext);
const buttonIcon = useGetButtonIcon();
@@ -99,6 +103,7 @@ export const RecordInlineCell = ({ readonly }: RecordInlineCellProps) => {
isDisplayModeContentEmpty={isFieldEmpty}
isDisplayModeFixHeight
editModeContentOnly={isFieldInputOnly}
+ loading={loading}
/>
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
index 185a9924996c..7400c712dafe 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
@@ -1,4 +1,5 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { Tooltip } from 'react-tooltip';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -82,6 +83,25 @@ const StyledTooltip = styled(Tooltip)`
padding: ${({ theme }) => theme.spacing(2)};
`;
+const StyledSkeletonDiv = styled.div`
+ height: 24px;
+`;
+
+const StyledInlineCellSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+ );
+};
+
type RecordInlineCellContainerProps = {
readonly?: boolean;
IconLabel?: IconComponent;
@@ -96,6 +116,7 @@ type RecordInlineCellContainerProps = {
isDisplayModeContentEmpty?: boolean;
isDisplayModeFixHeight?: boolean;
disableHoverEffect?: boolean;
+ loading?: boolean;
};
export const RecordInlineCellContainer = ({
@@ -112,6 +133,7 @@ export const RecordInlineCellContainer = ({
editModeContentOnly,
isDisplayModeFixHeight,
disableHoverEffect,
+ loading = false,
}: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const reference = useRef(null);
@@ -163,6 +185,43 @@ export const RecordInlineCellContainer = ({
}
}, [isHoveredForDisplayMode, displayModeContent, reference]);
+ const showContent = () => {
+ if (loading) {
+ return ;
+ }
+ return !readonly && isInlineCellInEditMode ? (
+ {editModeContent}
+ ) : editModeContentOnly ? (
+
+
+ {editModeContent}
+
+
+ ) : (
+
+
+ {newDisplayModeContent}
+
+ {showEditButton && }
+
+ );
+ };
+
return (
)}
- {!readonly && isInlineCellInEditMode ? (
- {editModeContent}
- ) : editModeContentOnly ? (
-
-
- {editModeContent}
-
-
- ) : (
-
-
- {newDisplayModeContent}
-
- {showEditButton && }
-
- )}
+ {showContent()}
);
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
index 475a3b0a5a18..d3dd52fc3722 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
@@ -36,11 +36,13 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type RecordShowContainerProps = {
objectNameSingular: string;
objectRecordId: string;
+ loading: boolean;
};
export const RecordShowContainer = ({
objectNameSingular,
objectRecordId,
+ loading,
}: RecordShowContainerProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -130,13 +132,14 @@ export const RecordShowContainer = ({
- {!recordLoading && isDefined(recordFromStore) && (
+ {isDefined(recordFromStore) && (
<>
-
+
))}
@@ -217,7 +223,9 @@ export const RecordShowContainer = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
-
+
))}
>
@@ -233,6 +241,7 @@ export const RecordShowContainer = ({
tasks
notes
emails
+ loading={loading || recordLoading}
/>
) : (
<>>
diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
index 9a4909e3ddc1..747179db336b 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx
@@ -1,4 +1,6 @@
import { useCallback, useContext } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import qs from 'qs';
import { useRecoilValue } from 'recoil';
@@ -27,11 +29,36 @@ import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+type RecordDetailRelationSectionProps = {
+ loading: boolean;
+};
+
const StyledAddDropdown = styled(Dropdown)`
margin-left: auto;
`;
-export const RecordDetailRelationSection = () => {
+const StyledSkeletonDiv = styled.div`
+ height: 40px;
+`;
+
+const StyledRecordDetailRelationSectionSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+ );
+};
+
+export const RecordDetailRelationSection = ({
+ loading,
+}: RecordDetailRelationSectionProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const {
fieldName,
@@ -113,6 +140,20 @@ export const RecordDetailRelationSection = () => {
relationObjectMetadataItem.namePlural
}?${qs.stringify(filterQueryParams)}`;
+ const showContent = () => {
+ if (loading) {
+ return ;
+ }
+
+ return relationRecords.length ? (
+
+ ) : (
+
+ );
+ };
+
return (
{
}
/>
- {relationRecords.length ? (
-
- ) : (
-
- )}
+ {showContent()}
);
};
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
index 32532a371a8b..fa81d538710d 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx
@@ -53,6 +53,7 @@ type ShowPageRightContainerProps = {
tasks?: boolean;
notes?: boolean;
emails?: boolean;
+ loading?: boolean;
};
export const ShowPageRightContainer = ({
@@ -61,6 +62,7 @@ export const ShowPageRightContainer = ({
tasks,
notes,
emails,
+ loading,
}: ShowPageRightContainerProps) => {
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState);
@@ -127,12 +129,16 @@ export const ShowPageRightContainer = ({
return (
-
+
{activeTabId === 'timeline' && (
<>
-
+
>
)}
{activeTabId === 'tasks' && (
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
index 8e5cf3dbfcb9..30b9085af956 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
@@ -1,5 +1,7 @@
import { ChangeEvent, ReactNode, useRef } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { Tooltip } from 'react-tooltip';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Avatar, AvatarType } from 'twenty-ui';
import { v4 as uuidV4 } from 'uuid';
@@ -18,9 +20,10 @@ type ShowPageSummaryCardProps = {
logoOrAvatar?: string;
onUploadPicture?: (file: File) => void;
title: ReactNode;
+ loading: boolean;
};
-const StyledShowPageSummaryCard = styled.div`
+export const StyledShowPageSummaryCard = styled.div`
align-items: center;
display: flex;
flex-direction: column;
@@ -28,6 +31,7 @@ const StyledShowPageSummaryCard = styled.div`
justify-content: center;
padding: ${({ theme }) => theme.spacing(4)};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
+ height: 127px;
`;
const StyledInfoContainer = styled.div`
@@ -70,6 +74,30 @@ const StyledFileInput = styled.input`
display: none;
`;
+const StyledSubSkeleton = styled.div`
+ align-items: center;
+ display: flex;
+ height: 37px;
+ justify-content: center;
+ width: 108px;
+`;
+
+const StyledShowPageSummaryCardSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+ );
+};
+
export const ShowPageSummaryCard = ({
avatarPlaceholder,
avatarType,
@@ -78,6 +106,7 @@ export const ShowPageSummaryCard = ({
logoOrAvatar,
onUploadPicture,
title,
+ loading,
}: ShowPageSummaryCardProps) => {
const beautifiedCreatedAt =
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
@@ -93,6 +122,13 @@ export const ShowPageSummaryCard = ({
inputFileRef?.current?.click?.();
};
+ if (loading)
+ return (
+
+
+
+ );
+
return (
diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
index d65cee222983..1d9f4baa5d0c 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
@@ -21,6 +21,7 @@ type SingleTabProps = {
type TabListProps = {
tabListId: string;
tabs: SingleTabProps[];
+ loading?: boolean;
};
const StyledContainer = styled.div`
@@ -33,7 +34,7 @@ const StyledContainer = styled.div`
user-select: none;
`;
-export const TabList = ({ tabs, tabListId }: TabListProps) => {
+export const TabList = ({ tabs, tabListId, loading }: TabListProps) => {
const initialActiveTabId = tabs[0].id;
const { activeTabIdState, setActiveTabId } = useTabList(tabListId);
@@ -60,7 +61,7 @@ export const TabList = ({ tabs, tabListId }: TabListProps) => {
onClick={() => {
setActiveTabId(tab.id);
}}
- disabled={tab.disabled}
+ disabled={tab.disabled ?? loading}
hasBetaPill={tab.hasBetaPill}
/>
))}
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
index 336f70523458..64887c0cbcaa 100644
--- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
@@ -110,31 +110,30 @@ export const RecordShowPage = () => {
Icon={headerIcon}
loading={loading}
>
- {record && (
- <>
-
-
-
- >
- )}
+ <>
+
+
+
+ >