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