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 fe248f8b4e5d..8ee67657592d 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
@@ -1,9 +1,8 @@
-import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
-import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
+import { TimelineSkeletonLoader } from '@/activities/timeline/components/TimelineSkeletonLoader';
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
@@ -29,82 +28,19 @@ 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;
+ loading: boolean;
}) => {
const timelineActivitiesForGroup = useRecoilValue(
timelineActivitiesForGroupState,
);
- if (loading === true) {
- return ;
+ if (loading) {
+ return ;
}
if (timelineActivitiesForGroup.length === 0) {
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx
new file mode 100644
index 000000000000..f328fda44085
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx
@@ -0,0 +1,72 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+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 = ({
+ isSecondColumn,
+}: {
+ isSecondColumn: boolean;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+ );
+};
+
+export const TimelineSkeletonLoader = () => {
+ const theme = useTheme();
+ const skeletonItems = Array.from({ length: 3 }).map((_, index) => ({
+ id: `skeleton-item-${index}`,
+ }));
+
+ return (
+
+
+
+ {skeletonItems.map(({ id }, index) => (
+
+
+
+
+
+ {index === 1 && }
+
+
+ ))}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx
index ec868b372372..c3bfeeb6aefe 100644
--- a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx
+++ b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx
@@ -1,6 +1,8 @@
import styled from '@emotion/styled';
import { Avatar } from 'twenty-ui';
+import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
+import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
@@ -32,6 +34,11 @@ const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
export const Favorites = () => {
const { favorites, handleReorderFavorite } = useFavorites();
+ const loading = useIsPrefetchLoading();
+
+ if (loading) {
+ return ;
+ }
if (!favorites || favorites.length === 0) return <>>;
diff --git a/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx b/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx
new file mode 100644
index 000000000000..3822a12bc9b6
--- /dev/null
+++ b/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx
@@ -0,0 +1,36 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+const StyledSkeletonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+ height: 71px;
+ padding-left: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledSkeletonColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const FavoritesSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
index 61526a42e208..cb28ad6d31d2 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
@@ -1,7 +1,9 @@
import { useLocation } from 'react-router-dom';
import { useIcons } from 'twenty-ui';
+import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
+import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
@@ -14,6 +16,11 @@ export const ObjectMetadataNavItems = () => {
const currentPath = useLocation().pathname;
const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+ const loading = useIsPrefetchLoading();
+
+ if (loading) {
+ return ;
+ }
return (
<>
diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader.tsx
new file mode 100644
index 000000000000..82669d907204
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader.tsx
@@ -0,0 +1,28 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+const StyledSkeletonColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(1)};
+ height: 76px;
+ padding-left: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const ObjectMetadataNavItemsSkeletonLoader: React.FC = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/components/__stories__/ObjectMetadataNavItems.stories.tsx b/packages/twenty-front/src/modules/object-metadata/components/__stories__/ObjectMetadataNavItems.stories.tsx
index 6efdf175bb5a..0edf2a209877 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/__stories__/ObjectMetadataNavItems.stories.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/__stories__/ObjectMetadataNavItems.stories.tsx
@@ -6,6 +6,7 @@ import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/Componen
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
+import { PrefetchLoadingDecorator } from '~/testing/decorators/PrefetchLoadingDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
@@ -20,6 +21,7 @@ const meta: Meta = {
ComponentWithRouterDecorator,
ComponentWithRecoilScopeDecorator,
SnackBarDecorator,
+ PrefetchLoadingDecorator,
],
parameters: {
msw: graphqlMocks,
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx
new file mode 100644
index 000000000000..38ee35d74787
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx
@@ -0,0 +1,31 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+const StyledSkeletonDiv = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ width: 100%;
+ height: 24px;
+`;
+export const PropertyBoxSkeletonLoader = () => {
+ const theme = useTheme();
+ const skeletonItems = Array.from({ length: 4 }).map((_, index) => ({
+ id: `skeleton-item-${index}`,
+ }));
+ return (
+
+ {skeletonItems.map(({ id }) => (
+
+
+
+
+ ))}
+
+ );
+};
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 d3dd52fc3722..87667d22f25f 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
@@ -12,6 +12,7 @@ import {
} from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
+import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
@@ -19,6 +20,7 @@ import { recordLoadingFamilyState } from '@/object-record/record-store/states/re
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
+import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
@@ -127,6 +129,7 @@ export const RecordShowContainer = ({
);
const isReadOnly = objectMetadataItem.isRemote;
+ const isPrefetchLoading = useIsPrefetchLoading();
return (
@@ -139,7 +142,7 @@ export const RecordShowContainer = ({
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
- loading={loading || recordLoading}
+ loading={isPrefetchLoading || loading || recordLoading}
title={
- {inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
-
-
-
- ))}
+ {isPrefetchLoading ? (
+
+ ) : (
+ inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
+
+
+
+ ))
+ )}
))}
@@ -241,7 +248,7 @@ export const RecordShowContainer = ({
tasks
notes
emails
- loading={loading || recordLoading}
+ loading={isPrefetchLoading || 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 747179db336b..a4a1b6b2e42a 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,6 +1,4 @@
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';
@@ -13,6 +11,7 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailRelationRecordsListEmptyState } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListEmptyState';
+import { RecordDetailRelationSectionSkeletonLoader } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionSkeletonLoader';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -37,25 +36,6 @@ const StyledAddDropdown = styled(Dropdown)`
margin-left: auto;
`;
-const StyledSkeletonDiv = styled.div`
- height: 40px;
-`;
-
-const StyledRecordDetailRelationSectionSkeletonLoader = () => {
- const theme = useTheme();
- return (
-
-
-
-
-
- );
-};
-
export const RecordDetailRelationSection = ({
loading,
}: RecordDetailRelationSectionProps) => {
@@ -142,7 +122,11 @@ export const RecordDetailRelationSection = ({
const showContent = () => {
if (loading) {
- return ;
+ return (
+
+ );
}
return relationRecords.length ? (
diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionSkeletonLoader.tsx
new file mode 100644
index 000000000000..12ba33e3b210
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionSkeletonLoader.tsx
@@ -0,0 +1,31 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+const StyledSkeletonDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(4)};
+ height: 40px;
+`;
+
+export const RecordDetailRelationSectionSkeletonLoader = ({
+ numSkeletons = 1,
+}: {
+ numSkeletons?: number;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+ {Array.from({ length: numSkeletons }).map((_, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts b/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts
new file mode 100644
index 000000000000..bbe7ff60446c
--- /dev/null
+++ b/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts
@@ -0,0 +1,14 @@
+import { useRecoilValue } from 'recoil';
+
+import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
+
+export const useIsPrefetchLoading = () => {
+ const areViewsPrefetched = useRecoilValue(
+ prefetchIsLoadedFamilyState(PrefetchKey.AllViews),
+ );
+ const areFavoritesPrefetched = useRecoilValue(
+ prefetchIsLoadedFamilyState(PrefetchKey.AllFavorites),
+ );
+ return !areViewsPrefetched || !areFavoritesPrefetched;
+};
diff --git a/packages/twenty-front/src/modules/support/components/SupportChat.tsx b/packages/twenty-front/src/modules/support/components/SupportChat.tsx
index d39b2c4a835f..05da707b6128 100644
--- a/packages/twenty-front/src/modules/support/components/SupportChat.tsx
+++ b/packages/twenty-front/src/modules/support/components/SupportChat.tsx
@@ -7,6 +7,8 @@ import { IconHelpCircle } from 'twenty-ui';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { supportChatState } from '@/client-config/states/supportChatState';
+import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
+import { SupportChatSkeletonLoader } from '@/support/components/SupportChatSkeletonLoader';
import { Button } from '@/ui/input/button/components/Button';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { User } from '~/generated/graphql';
@@ -37,6 +39,7 @@ export const SupportChat = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const supportChat = useRecoilValue(supportChatState);
const [isFrontChatLoaded, setIsFrontChatLoaded] = useState(false);
+ const loading = useIsPrefetchLoading();
const configureFront = useCallback(
(
@@ -98,6 +101,10 @@ export const SupportChat = () => {
currentWorkspaceMember,
]);
+ if (loading) {
+ return ;
+ }
+
return isFrontChatLoaded ? (