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 ? (