diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 7032895aad..5bc8afa217 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -9,6 +9,7 @@ import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId'; import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromPersonId'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -18,6 +19,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { Section } from '@/ui/layout/section/components/Section'; import { TimelineCalendarEventsWithTotal } from '~/generated/graphql'; @@ -74,14 +76,16 @@ export const Calendar = ({ } = useCalendarEvents(timelineCalendarEvents || []); if (firstQueryLoading) { - // TODO: implement loader - return; + return ; } if (!firstQueryLoading && !timelineCalendarEvents?.length) { // TODO: change animated placeholder return ( - + diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx b/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx similarity index 62% rename from packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx rename to packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx index f328fda440..21478ed73c 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx @@ -18,18 +18,14 @@ const StyledSkeletonSubSection = styled.div` gap: ${({ theme }) => theme.spacing(4)}; `; -const StyledSkeletonColumn = styled.div` +const StyledSkeletonSubSectionContent = styled.div` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(3)}; justify-content: center; `; -const StyledSkeletonLoader = ({ - isSecondColumn, -}: { - isSecondColumn: boolean; -}) => { +const SkeletonColumnLoader = ({ height }: { height: number }) => { const theme = useTheme(); return ( - + ); }; -export const TimelineSkeletonLoader = () => { +export const SkeletonLoader = ({ + withSubSections = false, +}: { + withSubSections?: boolean; +}) => { const theme = useTheme(); const skeletonItems = Array.from({ length: 3 }).map((_, index) => ({ id: `skeleton-item-${index}`, @@ -56,16 +56,17 @@ export const TimelineSkeletonLoader = () => { > - {skeletonItems.map(({ id }, index) => ( - - - - - - {index === 1 && } - - - ))} + {withSubSections && + skeletonItems.map(({ id }, index) => ( + + + + + + {index === 1 && } + + + ))} ); diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx index d0766c517b..53038755dc 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { H1Title, H1TitleFontColor } from 'twenty-ui'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; -import { EmailLoader } from '@/activities/emails/components/EmailLoader'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging'; import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId'; @@ -16,6 +16,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { Card } from '@/ui/layout/card/components/Card'; import { Section } from '@/ui/layout/section/components/Section'; @@ -61,12 +62,15 @@ export const EmailThreads = ({ const { totalNumberOfThreads, timelineThreads } = data?.[queryName] ?? {}; if (firstQueryLoading) { - return ; + return ; } if (!firstQueryLoading && !timelineThreads?.length) { return ( - + diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx index aacc0ec6ad..482b8e18f8 100644 --- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx @@ -1,8 +1,8 @@ import { ChangeEvent, useRef, useState } from 'react'; import styled from '@emotion/styled'; -import { isNonEmptyArray } from '@sniptt/guards'; import { IconPlus } from 'twenty-ui'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { AttachmentList } from '@/activities/files/components/AttachmentList'; import { DropZone } from '@/activities/files/components/DropZone'; import { useAttachments } from '@/activities/files/hooks/useAttachments'; @@ -15,6 +15,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { isDefined } from '~/utils/isDefined'; @@ -41,7 +42,7 @@ export const Attachments = ({ targetableObject: ActivityTargetableObject; }) => { const inputFileRef = useRef(null); - const { attachments } = useAttachments(targetableObject); + const { attachments, loading } = useAttachments(targetableObject); const { uploadAttachmentFile } = useUploadAttachmentFile(); const [isDraggingFile, setIsDraggingFile] = useState(false); @@ -58,7 +59,13 @@ export const Attachments = ({ await uploadAttachmentFile(file, targetableObject); }; - if (!isNonEmptyArray(attachments)) { + const isAttachmentsEmpty = !attachments || attachments.length === 0; + + if (loading && isAttachmentsEmpty) { + return ; + } + + if (isAttachmentsEmpty) { return ( setIsDraggingFile(true)}> {isDraggingFile ? ( @@ -67,7 +74,10 @@ export const Attachments = ({ onUploadFile={onUploadFile} /> ) : ( - + diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx index cb8a1837d8..a91eb40313 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -4,13 +4,12 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -// do we need to test this? export const useAttachments = (targetableObject: ActivityTargetableObject) => { const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: targetableObject.targetObjectNameSingular, }); - const { records: attachments } = useFindManyRecords({ + const { records: attachments, loading } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, filter: { [targetableObjectFieldIdName]: { @@ -26,5 +25,6 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { return { attachments, + loading, }; }; diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx index 173bda4ede..4f3e83656e 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { IconPlus } from 'twenty-ui'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { NoteList } from '@/activities/notes/components/NoteList'; import { useNotes } from '@/activities/notes/hooks/useNotes'; @@ -12,6 +13,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; const StyledNotesContainer = styled.div` @@ -27,13 +29,22 @@ export const Notes = ({ }: { targetableObject: ActivityTargetableObject; }) => { - const { notes } = useNotes(targetableObject); + const { notes, loading } = useNotes(targetableObject); const openCreateActivity = useOpenCreateActivityDrawer(); - if (notes?.length === 0) { + const isNotesEmpty = !notes || notes.length === 0; + + if (loading && isNotesEmpty) { + return ; + } + + if (isNotesEmpty) { return ( - + @@ -62,7 +73,7 @@ export const Notes = ({ ; + } + + if (isTasksEmpty) { return ( - + diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts index f7938d17bc..e560e26079 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts @@ -107,17 +107,19 @@ export const useTasks = ({ setCurrentIncompleteTaskQueryVariables, ]); - const { activities: completeTasksData } = useActivities({ - targetableObjects, - activitiesFilters: completedQueryVariables.filter ?? {}, - activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}], - }); + const { activities: completeTasksData, loading: completeTasksLoading } = + useActivities({ + targetableObjects, + activitiesFilters: completedQueryVariables.filter ?? {}, + activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}], + }); - const { activities: incompleteTaskData } = useActivities({ - targetableObjects, - activitiesFilters: incompleteQueryVariables.filter ?? {}, - activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}], - }); + const { activities: incompleteTaskData, loading: incompleteTasksLoading } = + useActivities({ + targetableObjects, + activitiesFilters: incompleteQueryVariables.filter ?? {}, + activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}], + }); const todayOrPreviousTasks = incompleteTaskData?.filter((task) => { if (!task.dueAt) { @@ -148,5 +150,7 @@ export const useTasks = ({ upcomingTasks: (upcomingTasks ?? []) as Activity[], unscheduledTasks: (unscheduledTasks ?? []) as Activity[], completedTasks: (completedTasks ?? []) as Activity[], + completeTasksLoading, + incompleteTasksLoading, }; }; 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 8ee6765759..a6b2fd9b07 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; 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'; @@ -11,6 +11,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -40,12 +41,15 @@ export const Timeline = ({ ); if (loading) { - return ; + return ; } if (timelineActivitiesForGroup.length === 0) { return ( - + diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx index 9b0dca3e8a..b64f624e33 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import { isNonEmptyArray } from '@sniptt/guards'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup'; import { EventList } from '@/activities/timelineActivities/components/EventList'; import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; @@ -12,6 +12,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -40,9 +41,19 @@ export const TimelineActivities = ({ const { timelineActivities, loading, fetchMoreRecords } = useTimelineActivities(targetableObject); - if (!isNonEmptyArray(timelineActivities)) { + const isTimelineActivitiesEmpty = + !timelineActivities || timelineActivities.length === 0; + + if (loading && isTimelineActivitiesEmpty) { + return ; + } + + if (isTimelineActivitiesEmpty) { return ( - + diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx b/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx index 9d98e821f6..d088ec7e53 100644 --- a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx +++ b/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; -const StyledEmptyContainer = styled.div` +const StyledEmptyContainer = styled(motion.div)` align-items: center; width: 100%; height: 100%; @@ -13,6 +14,14 @@ const StyledEmptyContainer = styled.div` export { StyledEmptyContainer as AnimatedPlaceholderEmptyContainer }; +export const EMPTY_PLACEHOLDER_TRANSITION_PROPS = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + transition: { + duration: 0.15, + }, +}; + const StyledEmptyTextContainer = styled.div` align-items: center; display: flex;