diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx
new file mode 100644
index 000000000..9920568fc
--- /dev/null
+++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx
@@ -0,0 +1,74 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import styled from '@emotion/styled';
+import { motion } from 'framer-motion';
+import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui';
+
+import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths';
+import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader';
+
+const StyledAnimatedContainer = styled(motion.div)`
+ display: flex;
+ justify-content: end;
+`;
+
+const StyledItemsContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ margin-bottom: auto;
+ overflow-y: auto;
+ height: calc(100vh - 32px);
+ min-width: 216px;
+ max-width: 216px;
+`;
+
+const StyledSkeletonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+`;
+
+const StyledSkeletonTitleContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-left: 12px;
+ margin-top: 8px;
+`;
+
+export const LeftPanelSkeletonLoader = () => {
+ const isMobile = useIsMobile();
+ const mobileWidth = isMobile ? 0 : '100%';
+ const desktopWidth = !mobileWidth ? 12 : DESKTOP_NAV_DRAWER_WIDTHS.menu;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx
new file mode 100644
index 000000000..b13b1bb88
--- /dev/null
+++ b/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx
@@ -0,0 +1,34 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import styled from '@emotion/styled';
+import { BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui';
+
+const StyledSkeletonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-left: 12px;
+ margin-top: 8px;
+`;
+
+export const MainNavigationDrawerItemsSkeletonLoader = ({
+ title,
+ length,
+}: {
+ title?: boolean;
+ length: number;
+}) => {
+ return (
+
+
+ {title && }
+ {Array.from({ length }).map((_, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx
new file mode 100644
index 000000000..e42dc5fec
--- /dev/null
+++ b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx
@@ -0,0 +1,92 @@
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import styled from '@emotion/styled';
+import {
+ BACKGROUND_LIGHT,
+ BORDER_COMMON,
+ BORDER_LIGHT,
+ GRAY_SCALE,
+ MOBILE_VIEWPORT,
+} from 'twenty-ui';
+
+const StyledMainContainer = styled.div`
+ background: ${BACKGROUND_LIGHT.noisy};
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: row;
+ gap: 8px;
+ min-height: 0;
+ padding-left: 0;
+ width: 100%;
+
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ padding-left: 12px;
+ padding-bottom: 0;
+ }
+`;
+
+const StyledPanel = styled.div`
+ background: ${BACKGROUND_LIGHT.primary};
+ border: 1px solid ${BORDER_LIGHT.color.medium};
+ border-radius: ${BORDER_COMMON.radius.md};
+ height: 100%;
+ overflow: auto;
+ width: 100%;
+`;
+
+const StyledHeaderContainer = styled.div`
+ flex: 1;
+`;
+const StyledRightPanelContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+const StyledRightPanelFlexContainer = styled.div`
+ display: flex;
+ margin-top: 12px;
+ margin-bottom: 14px;
+`;
+
+const StyledSkeletonHeaderLoader = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+const StyledSkeletonAddLoader = () => {
+ return (
+
+
+
+ );
+};
+
+const RightPanelSkeleton = () => (
+
+
+
+);
+
+export const RightPanelSkeletonLoader = () => (
+
+
+
+
+
+
+
+);
diff --git a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx
new file mode 100644
index 000000000..d4d078619
--- /dev/null
+++ b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx
@@ -0,0 +1,32 @@
+import styled from '@emotion/styled';
+import { BACKGROUND_LIGHT, MOBILE_VIEWPORT } from 'twenty-ui';
+
+import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths';
+import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
+import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
+
+const StyledContainer = styled.div`
+ background: ${BACKGROUND_LIGHT.noisy};
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+ height: 100vh;
+ min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px;
+ width: 100%;
+ padding: 12px 8px 12px;
+ overflow: hidden;
+
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ width: 100%;
+ }
+`;
+
+export const UserOrMetadataLoader = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/loading/hooks/useUserOrMetadataLoading.ts b/packages/twenty-front/src/loading/hooks/useUserOrMetadataLoading.ts
new file mode 100644
index 000000000..b35cdb3dd
--- /dev/null
+++ b/packages/twenty-front/src/loading/hooks/useUserOrMetadataLoading.ts
@@ -0,0 +1,11 @@
+import { useRecoilValue } from 'recoil';
+
+import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+
+export const useUserOrMetadataLoading = () => {
+ const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ return !isCurrentUserLoaded || objectMetadataItems.length === 0;
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx
index 85f240db9..b374d13e1 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
+import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const ObjectMetadataItemsProvider = ({
children,
@@ -15,10 +16,12 @@ export const ObjectMetadataItemsProvider = ({
return (
<>
- {shouldDisplayChildren && (
+ {shouldDisplayChildren ? (
{children}
+ ) : (
+
)}
>
);
diff --git a/packages/twenty-front/src/modules/users/components/UserProvider.tsx b/packages/twenty-front/src/modules/users/components/UserProvider.tsx
index 5287cb4e2..806bcf35e 100644
--- a/packages/twenty-front/src/modules/users/components/UserProvider.tsx
+++ b/packages/twenty-front/src/modules/users/components/UserProvider.tsx
@@ -2,9 +2,10 @@ import React from 'react';
import { useRecoilValue } from 'recoil';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
+import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const UserProvider = ({ children }: React.PropsWithChildren) => {
const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
- return !isCurrentUserLoaded ? <>> : <>{children}>;
+ return !isCurrentUserLoaded ? : <>{children}>;
};