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 && ( - <> - - - - - )} + <> + + + +