diff --git a/packages/twenty-docs/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/developers/extend/apps/layout/page-layouts.mdx index 8e11f88afc..aa04200c98 100644 --- a/packages/twenty-docs/developers/extend/apps/layout/page-layouts.mdx +++ b/packages/twenty-docs/developers/extend/apps/layout/page-layouts.mdx @@ -31,7 +31,7 @@ export default definePageLayout({ title: 'Hello World', position: 50, icon: 'IconWorld', - layoutMode: PageLayoutTabLayoutMode.CANVAS, + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, widgets: [ { universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', @@ -53,7 +53,7 @@ export default definePageLayout({ - `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. - `objectUniversalIdentifier` specifies which object this layout applies to. -- Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +- Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`VERTICAL_LIST` for record pages, `GRID` for dashboards). A tab holding a single widget renders it full-bleed automatically; with several widgets they stack as cards. - Each `widget` inside a tab can render a [front component](/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. - `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. @@ -77,7 +77,7 @@ export default definePageLayoutTab({ title: 'Hello World', position: 1000, icon: 'IconWorld', - layoutMode: PageLayoutTabLayoutMode.CANVAS, + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, widgets: [ { universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', diff --git a/packages/twenty-docs/developers/extend/apps/tutorials/document-generator/building-the-ui.mdx b/packages/twenty-docs/developers/extend/apps/tutorials/document-generator/building-the-ui.mdx index b2309049ef..340e0690ab 100644 --- a/packages/twenty-docs/developers/extend/apps/tutorials/document-generator/building-the-ui.mdx +++ b/packages/twenty-docs/developers/extend/apps/tutorials/document-generator/building-the-ui.mdx @@ -220,8 +220,8 @@ export default defineFrontComponent({ ``` Mount it with a [page layout](/developers/extend/apps/layout/page-layouts). A -`RECORD_PAGE` layout adds tabs to an object's record view; a `FRONT_COMPONENT` -widget in a `CANVAS` tab hosts the component: +`RECORD_PAGE` layout adds tabs to an object's record view; a tab with a single +`FRONT_COMPONENT` widget hosts the component full-bleed: ```ts filename="src/page-layouts/document-record.page-layout.ts" import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; @@ -236,7 +236,7 @@ export default definePageLayout({ title: 'Preview', icon: 'IconEye', position: 50, - layoutMode: PageLayoutTabLayoutMode.CANVAS, + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, widgets: [{ universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER, title: 'Document preview', diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx index dba5317f2b..dda1ccabab 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx @@ -12,6 +12,7 @@ import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomRes import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { useSubscribeTimelineToParticipantChanges } from '@/activities/hooks/useSubscribeTimelineToParticipantChanges'; +import { usePublishWidgetHeaderInfo } from '@/page-layout/widgets/hooks/usePublishWidgetHeaderInfo'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { H3Title } from 'twenty-ui/typography'; import { @@ -69,6 +70,8 @@ export const CalendarEventsCard = () => { const { timelineCalendarEvents, totalNumberOfCalendarEvents } = data?.getTimelineCalendarEventsFromObjectRecord ?? {}; + usePublishWidgetHeaderInfo({ count: totalNumberOfCalendarEvents }); + const { calendarEventsByDayTime, daysByMonthTime, diff --git a/packages/twenty-front/src/modules/activities/emails/components/ComposeEmailButton.tsx b/packages/twenty-front/src/modules/activities/emails/components/ComposeEmailButton.tsx deleted file mode 100644 index 4369f9a3a1..0000000000 --- a/packages/twenty-front/src/modules/activities/emails/components/ComposeEmailButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useComposeEmailForTargetRecord } from '@/activities/emails/hooks/useComposeEmailForTargetRecord'; -import { IconPlus } from 'twenty-ui/icon'; -import { LightIconButton } from 'twenty-ui/input'; - -export const ComposeEmailButton = () => { - const { openComposer, loading } = useComposeEmailForTargetRecord(); - - return ( - - ); -}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx index f310a2c6f3..2712dd38a7 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx @@ -4,16 +4,18 @@ import { CoreObjectNameSingular } from 'twenty-shared/types'; import { ActivityList } from '@/activities/components/ActivityList'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; -import { ComposeEmailButton } from '@/activities/emails/components/ComposeEmailButton'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; import { EmptyInboxPlaceholder } from '@/activities/emails/components/EmptyInboxPlaceholder'; import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging'; import { getTimelineThreadsFromObjectRecord } from '@/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord'; +import { useComposeEmailForTargetRecord } from '@/activities/emails/hooks/useComposeEmailForTargetRecord'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { useSubscribeTimelineToParticipantChanges } from '@/activities/hooks/useSubscribeTimelineToParticipantChanges'; +import { usePublishWidgetHeaderInfo } from '@/page-layout/widgets/hooks/usePublishWidgetHeaderInfo'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; -import { Trans } from '@lingui/react/macro'; -import { H1Title, H1TitleFontColor } from 'twenty-ui/typography'; +import { t } from '@lingui/core/macro'; +import { useMemo } from 'react'; +import { IconPlus } from 'twenty-ui/icon'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { @@ -31,25 +33,10 @@ const StyledContainer = styled.div` ${themeCssVariables.spacing[2]}; `; -const StyledHeaderRow = styled.div` - align-items: center; - display: flex; - justify-content: space-between; - margin-bottom: ${themeCssVariables.spacing[4]}; -`; - -const StyledH1Title = styled(H1Title)` - display: flex; - gap: ${themeCssVariables.spacing[2]}; - margin-bottom: 0; -`; - -const StyledEmailCount = styled.span` - color: ${themeCssVariables.font.color.light}; -`; - export const EmailsCard = () => { const targetRecord = useTargetRecord(); + const { openComposer, loading: composerLoading } = + useComposeEmailForTargetRecord(); const { data, firstQueryLoading, isFetchingMore, fetchMoreRecords, refetch } = useCustomResolver( @@ -70,6 +57,22 @@ export const EmailsCard = () => { const { totalNumberOfThreads, timelineThreads } = data?.getTimelineThreadsFromObjectRecord ?? {}; + + const composeAction = useMemo( + () => ({ + Icon: IconPlus, + label: t`Compose`, + onClick: openComposer, + disabled: composerLoading, + }), + [openComposer, composerLoading], + ); + + usePublishWidgetHeaderInfo({ + count: totalNumberOfThreads, + primaryAction: composeAction, + }); + const hasMoreTimelineThreads = timelineThreads && totalNumberOfThreads ? timelineThreads?.length < totalNumberOfThreads @@ -96,18 +99,6 @@ export const EmailsCard = () => { return (
- - - Inbox{' '} - {totalNumberOfThreads} - - } - fontColor={H1TitleFontColor.Primary} - /> - - {!firstQueryLoading && ( {timelineThreads?.map((thread: TimelineThread) => ( diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx index 8919828c8d..0af9f98267 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx @@ -1,6 +1,6 @@ import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; -import { lazy, type ReactElement, Suspense, useState } from 'react'; +import { lazy, Suspense, useState } from 'react'; import { createPortal } from 'react-dom'; import { DropZone } from '@/activities/files/components/DropZone'; @@ -37,9 +37,7 @@ const DocumentViewer = lazy(() => type AttachmentListProps = { targetableObject: ActivityTargetableObject; - title: string; attachments: Attachment[]; - button?: ReactElement | false | null; }; const StyledContainer = styled.div` @@ -49,30 +47,11 @@ const StyledContainer = styled.div` flex-direction: column; height: 100%; justify-content: center; - padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[6]} + padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[6]}; width: calc(100% - ${themeCssVariables.spacing[12]}); `; -const StyledTitleBar = styled.h3` - display: flex; - justify-content: space-between; - margin-bottom: ${themeCssVariables.spacing[4]}; - margin-top: ${themeCssVariables.spacing[4]}; - place-items: center; - width: 100%; -`; - -const StyledTitle = styled.span` - color: ${themeCssVariables.font.color.primary}; - font-weight: ${themeCssVariables.font.weight.semiBold}; -`; - -const StyledCount = styled.span` - color: ${themeCssVariables.font.color.light}; - margin-left: ${themeCssVariables.spacing[2]}; -`; - const StyledDropZoneContainer = styled.div` height: 100%; overflow: auto; @@ -116,9 +95,7 @@ export const PREVIEW_MODAL_ID = 'preview-modal'; export const AttachmentList = ({ targetableObject, - title, attachments, - button, }: AttachmentListProps) => { const { uploadAttachmentFile } = useUploadAttachmentFile(); const [isDraggingFile, setIsDraggingFile] = useState(false); @@ -172,12 +149,6 @@ export const AttachmentList = ({ <> {attachmentsWithFile.length > 0 && ( - - - {title} {attachmentsWithFile.length} - - {button} - hasUploadPermission && setIsDraggingFile(true)} > diff --git a/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx b/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx index 4ec552daf8..acbf2bef11 100644 --- a/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx @@ -1,5 +1,5 @@ import { styled } from '@linaria/react'; -import { type ChangeEvent, useRef, useState } from 'react'; +import { type ChangeEvent, useMemo, useRef, useState } from 'react'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { AttachmentList } from '@/activities/files/components/AttachmentList'; @@ -8,6 +8,7 @@ import { useAttachments } from '@/activities/files/hooks/useAttachments'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; +import { usePublishWidgetHeaderInfo } from '@/page-layout/widgets/hooks/usePublishWidgetHeaderInfo'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { Trans, useLingui } from '@lingui/react/macro'; @@ -42,7 +43,8 @@ const StyledDropZoneContainer = styled.div` export const FilesCard = () => { const targetRecord = useTargetRecord(); const inputFileRef = useRef(null); - const { attachments, loading } = useAttachments(targetRecord); + const { attachments, loading, totalCountAttachments } = + useAttachments(targetRecord); const { uploadAttachmentFile } = useUploadAttachmentFile(); const [isDraggingFile, setIsDraggingFile] = useState(false); @@ -87,6 +89,23 @@ export const FilesCard = () => { const canUploadFiles = hasObjectUpdatePermissions && hasUploadPermission; + const addFileAction = useMemo( + () => + canUploadFiles + ? { + Icon: IconPlus, + label: t`Add file`, + onClick: () => inputFileRef?.current?.click?.(), + } + : undefined, + [canUploadFiles, t], + ); + + usePublishWidgetHeaderInfo({ + count: totalCountAttachments, + primaryAction: addFileAction, + }); + if (loading && isAttachmentsEmpty) { return ; } @@ -142,19 +161,7 @@ export const FilesCard = () => { /> - ) - } /> ); 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 caa3889dad..e89d7f039a 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -8,7 +8,11 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { nameSingular: targetableObject.targetObjectNameSingular, }); - const { records: attachments, loading } = useFindManyRecords({ + const { + records: attachments, + loading, + totalCount, + } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, filter: { [targetableObjectFieldIdName]: { @@ -25,5 +29,6 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { return { attachments, loading, + totalCountAttachments: totalCount, }; }; diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx index 7e9bdccdd4..05f06e6d2f 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteList.tsx @@ -1,17 +1,12 @@ import { styled } from '@linaria/react'; -import { type ReactElement } from 'react'; import { type Note } from '@/activities/types/Note'; -import { useNumberFormat } from '@/localization/hooks/useNumberFormat'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { NoteTile } from './NoteTile'; type NoteListProps = { - title: string; notes: Note[]; - button?: ReactElement | false | null; - totalCount: number; }; const StyledContainer = styled.div` @@ -20,26 +15,7 @@ const StyledContainer = styled.div` display: flex; flex-direction: column; justify-content: center; - padding: 8px 24px; -`; - -const StyledTitleBar = styled.h3` - display: flex; - justify-content: space-between; - margin-bottom: ${themeCssVariables.spacing[4]}; - margin-top: ${themeCssVariables.spacing[4]}; - place-items: center; - width: 100%; -`; - -const StyledTitle = styled.span` - color: ${themeCssVariables.font.color.primary}; - font-weight: ${themeCssVariables.font.weight.semiBold}; -`; - -const StyledCount = styled.span` - color: ${themeCssVariables.font.color.light}; - margin-left: ${themeCssVariables.spacing[2]}; + padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[6]}; `; const StyledNoteContainer = styled.div` @@ -50,24 +26,11 @@ const StyledNoteContainer = styled.div` width: 100%; `; -export const NoteList = ({ - title, - notes, - totalCount, - button, -}: NoteListProps) => { - const { formatNumber } = useNumberFormat(); - +export const NoteList = ({ notes }: NoteListProps) => { return ( <> {notes.length > 0 && ( - - - {title} {formatNumber(totalCount)} - - {button} - {notes.map((note) => ( { const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + const newNoteAction = useMemo( + () => + hasObjectUpdatePermissions + ? { + Icon: IconPlus, + label: t`New note`, + onClick: () => + openCreateActivity({ targetableObjects: [targetRecord] }), + } + : undefined, + [hasObjectUpdatePermissions, openCreateActivity, targetRecord], + ); + + usePublishWidgetHeaderInfo({ + count: totalCountNotes, + primaryAction: newNoteAction, + }); + if (loading && isNotesEmpty) { return ; } @@ -88,26 +108,7 @@ export const NotesCard = () => { return ( - - openCreateActivity({ - targetableObjects: [targetRecord], - }) - } - /> - ) - } - /> + { - const openCreateActivity = useOpenCreateActivityDrawer({ - activityObjectNameSingular: CoreObjectNameSingular.Task, - }); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: activityTargetableObject.targetObjectNameSingular, - }); - - const objectPermissions = useObjectPermissionsForObject( - objectMetadataItem.id, - ); - - const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; - - if (!hasObjectUpdatePermissions) { - return null; - } - - return ( -