diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
similarity index 92%
rename from packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
rename to packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
index f0f7cc46a0..e2108387e1 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
@@ -12,8 +12,8 @@ import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { H3Title } from 'twenty-ui/display';
import {
AnimatedPlaceholder,
@@ -45,21 +45,17 @@ const StyledTitleContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
-export const Calendar = ({
- targetableObject,
-}: {
- targetableObject: ActivityTargetableObject;
-}) => {
+export const CalendarEventsCard = () => {
+ const targetRecord = useTargetRecord();
const { localeCatalog } = useRecoilValue(dateLocaleState);
const [query, queryName] =
- targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person
+ targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Person
? [
getTimelineCalendarEventsFromPersonId,
'getTimelineCalendarEventsFromPersonId',
]
- : targetableObject.targetObjectNameSingular ===
- CoreObjectNameSingular.Company
+ : targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Company
? [
getTimelineCalendarEventsFromCompanyId,
'getTimelineCalendarEventsFromCompanyId',
@@ -74,12 +70,20 @@ export const Calendar = ({
query,
queryName,
'timelineCalendarEvents',
- targetableObject,
+ targetRecord,
TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
);
const { timelineCalendarEvents, totalNumberOfCalendarEvents } =
data?.[queryName] ?? {};
+
+ const {
+ calendarEventsByDayTime,
+ daysByMonthTime,
+ monthTimes,
+ monthTimesByYear,
+ } = useCalendarEvents(timelineCalendarEvents || []);
+
const hasMoreCalendarEvents =
timelineCalendarEvents && totalNumberOfCalendarEvents
? timelineCalendarEvents?.length < totalNumberOfCalendarEvents
@@ -91,13 +95,6 @@ export const Calendar = ({
}
};
- const {
- calendarEventsByDayTime,
- daysByMonthTime,
- monthTimes,
- monthTimesByYear,
- } = useCalendarEvents(timelineCalendarEvents || []);
-
if (firstQueryLoading) {
return ;
}
@@ -116,7 +113,7 @@ export const Calendar = ({
No events have been scheduled with this{' '}
- {targetableObject.targetObjectNameSingular} yet.
+ {targetRecord.targetObjectNameSingular} yet.
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
index d5ec752c23..ea0502a876 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
@@ -2,23 +2,40 @@ import { getOperationName } from '@apollo/client/utilities';
import { type Meta, type StoryObj } from '@storybook/react';
import { HttpResponse, graphql } from 'msw';
-import { Calendar } from '@/activities/calendar/components/Calendar';
+import { CalendarEventsCard } from '@/activities/calendar/components/CalendarEventsCard';
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { ComponentDecorator } from 'twenty-ui/testing';
+import { PageLayoutType } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTimelineCalendarEvents } from '~/testing/mock-data/timeline-calendar-events';
-const meta: Meta = {
- title: 'Modules/Activities/Calendar/Calendar',
- component: Calendar,
+const meta: Meta = {
+ title: 'Modules/Activities/Calendar/CalendarEventsCard',
+ component: CalendarEventsCard,
decorators: [
I18nFrontDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
+ (Story) => (
+
+
+
+ ),
],
parameters: {
container: { width: 728 },
@@ -53,15 +70,9 @@ const meta: Meta = {
],
},
},
- args: {
- targetableObject: {
- id: '1',
- targetObjectNameSingular: 'Company',
- },
- },
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
similarity index 91%
rename from packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
rename to packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
index 792865f25e..047a3fe63d 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
@@ -9,8 +9,8 @@ import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/que
import { getTimelineThreadsFromOpportunityId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId';
import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { Trans } from '@lingui/react/macro';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import {
@@ -45,16 +45,13 @@ const StyledEmailCount = styled.span`
color: ${({ theme }) => theme.font.color.light};
`;
-export const EmailThreads = ({
- targetableObject,
-}: {
- targetableObject: ActivityTargetableObject;
-}) => {
+export const EmailsCard = () => {
+ const targetRecord = useTargetRecord();
+
const [query, queryName] =
- targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person
+ targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Person
? [getTimelineThreadsFromPersonId, 'getTimelineThreadsFromPersonId']
- : targetableObject.targetObjectNameSingular ===
- CoreObjectNameSingular.Company
+ : targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Company
? [getTimelineThreadsFromCompanyId, 'getTimelineThreadsFromCompanyId']
: [
getTimelineThreadsFromOpportunityId,
@@ -66,7 +63,7 @@ export const EmailThreads = ({
query,
queryName,
'timelineThreads',
- targetableObject,
+ targetRecord,
TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
);
diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx
similarity index 91%
rename from packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
rename to packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx
index 12a86447f5..421d9842df 100644
--- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
+++ b/packages/twenty-front/src/modules/activities/files/components/FilesCard.tsx
@@ -6,9 +6,9 @@ import { AttachmentList } from '@/activities/files/components/AttachmentList';
import { DropZone } from '@/activities/files/components/DropZone';
import { useAttachments } from '@/activities/files/hooks/useAttachments';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { Trans, useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
@@ -38,13 +38,10 @@ const StyledDropZoneContainer = styled.div`
height: 100%;
`;
-export const Attachments = ({
- targetableObject,
-}: {
- targetableObject: ActivityTargetableObject;
-}) => {
+export const FilesCard = () => {
+ const targetRecord = useTargetRecord();
const inputFileRef = useRef(null);
- const { attachments, loading } = useAttachments(targetableObject);
+ const { attachments, loading } = useAttachments(targetRecord);
const { uploadAttachmentFile } = useUploadAttachmentFile();
const [isDraggingFile, setIsDraggingFile] = useState(false);
@@ -52,7 +49,7 @@ export const Attachments = ({
const { t } = useLingui();
const onUploadFile = async (file: File) => {
- await uploadAttachmentFile(file, targetableObject);
+ await uploadAttachmentFile(file, targetRecord);
};
const onUploadFiles = async (files: File[]) => {
@@ -74,7 +71,7 @@ export const Attachments = ({
const isAttachmentsEmpty = !attachments || attachments.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
- objectNameSingular: targetableObject.targetObjectNameSingular,
+ objectNameSingular: targetRecord.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
@@ -138,7 +135,7 @@ export const Attachments = ({
multiple
/>
(
{notes.map((note) => (
- theme.spacing(4)});
`;
-export const NoteCard = ({
+export const NoteTile = ({
note,
isSingleNote,
}: {
diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/NotesCard.tsx
similarity index 87%
rename from packages/twenty-front/src/modules/activities/notes/components/Notes.tsx
rename to packages/twenty-front/src/modules/activities/notes/components/NotesCard.tsx
index 4b86ee9994..6baf041e89 100644
--- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx
+++ b/packages/twenty-front/src/modules/activities/notes/components/NotesCard.tsx
@@ -2,10 +2,10 @@ 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';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -26,12 +26,9 @@ const StyledNotesContainer = styled.div`
overflow: auto;
`;
-export const Notes = ({
- targetableObject,
-}: {
- targetableObject: ActivityTargetableObject;
-}) => {
- const { notes, loading } = useNotes(targetableObject);
+export const NotesCard = () => {
+ const targetRecord = useTargetRecord();
+ const { notes, loading } = useNotes(targetRecord);
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note,
@@ -40,7 +37,7 @@ export const Notes = ({
const isNotesEmpty = !notes || notes.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
- objectNameSingular: targetableObject.targetObjectNameSingular,
+ objectNameSingular: targetRecord.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
@@ -75,7 +72,7 @@ export const Notes = ({
variant="secondary"
onClick={() =>
openCreateActivity({
- targetableObjects: [targetableObject],
+ targetableObjects: [targetRecord],
})
}
/>
@@ -98,7 +95,7 @@ export const Notes = ({
title="Add note"
onClick={() =>
openCreateActivity({
- targetableObjects: [targetableObject],
+ targetableObjects: [targetRecord],
})
}
/>
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TasksCard.tsx
similarity index 68%
rename from packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx
rename to packages/twenty-front/src/modules/activities/tasks/components/TasksCard.tsx
index b985e7ee69..54bd8f5f73 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/TasksCard.tsx
@@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
const StyledContainer = styled.div`
display: flex;
@@ -12,17 +12,15 @@ const StyledContainer = styled.div`
overflow: auto;
`;
-type ObjectTasksProps = {
- targetableObject: ActivityTargetableObject;
-};
+export const TasksCard = () => {
+ const targetRecord = useTargetRecord();
-export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
return (
-
+
);
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCard.tsx
similarity index 88%
rename from packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx
rename to packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCard.tsx
index 5b4e3fbb51..5f687e4476 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCard.tsx
@@ -4,7 +4,8 @@ import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomRes
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { EventList } from '@/activities/timeline-activities/components/EventList';
import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
AnimatedPlaceholder,
@@ -44,15 +45,11 @@ const StyledRightDrawerAnimatedPlaceholderEmptyContainer = styled(
padding-top: ${({ theme }) => theme.spacing(8)};
`;
-export const TimelineActivities = ({
- targetableObject,
- isInRightDrawer,
-}: {
- targetableObject: ActivityTargetableObject;
- isInRightDrawer?: boolean;
-}) => {
+export const TimelineCard = () => {
+ const targetRecord = useTargetRecord();
+ const { isInRightDrawer } = useLayoutRenderingContext();
const { timelineActivities, loading, fetchMoreRecords } =
- useTimelineActivities(targetableObject);
+ useTimelineActivities(targetRecord);
const isTimelineActivitiesEmpty =
!timelineActivities || timelineActivities.length === 0;
@@ -87,7 +84,7 @@ export const TimelineActivities = ({
return (
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx
index 9a06151ff4..cbd38d610c 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx
@@ -1,34 +1,42 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { HttpResponse, graphql } from 'msw';
-import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities';
+import { TimelineCard } from '@/activities/timeline-activities/components/TimelineCard';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { ComponentDecorator } from 'twenty-ui/testing';
+import { PageLayoutType } from '~/generated/graphql';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities';
-import { ComponentDecorator } from 'twenty-ui/testing';
-const meta: Meta = {
- title: 'Modules/TimelineActivities/TimelineActivities',
- component: TimelineActivities,
+const meta: Meta = {
+ title: 'Modules/TimelineActivities/TimelineCard',
+ component: TimelineCard,
decorators: [
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
(Story) => {
return (
-
-
-
+
+
+
+
+
);
},
],
- args: {
- targetableObject: {
- id: '1',
- targetObjectNameSingular: 'company',
- },
- },
parameters: {
msw: {
handlers: [
@@ -71,6 +79,6 @@ const meta: Meta = {
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx
index bbd3f312e6..459584d5f8 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx
@@ -65,7 +65,8 @@ export const useCommandMenuSearchRecords = () => {
Icon: () => (
{
+ const targetRecord = useTargetRecord();
-export const DashboardRenderer = ({ recordId }: DashboardRendererProps) => {
const { record: dashboard } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Dashboard,
- objectRecordId: recordId,
+ objectRecordId: targetRecord.id,
});
if (!isDefined(dashboard)) {
diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
index 3f2dee9505..d387dc218e 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
@@ -1,4 +1,5 @@
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
@@ -8,12 +9,12 @@ import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { useRecoilValue } from 'recoil';
-const ORDERED_STANDARD_OBJECTS = [
- 'person',
- 'company',
- 'opportunity',
- 'task',
- 'note',
+const ORDERED_STANDARD_OBJECTS: string[] = [
+ CoreObjectNameSingular.Person,
+ CoreObjectNameSingular.Company,
+ CoreObjectNameSingular.Opportunity,
+ CoreObjectNameSingular.Task,
+ CoreObjectNameSingular.Note,
];
type NavigationDrawerSectionForObjectMetadataItemsProps = {
diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx
index 7c9771f8f4..635ff43e7a 100644
--- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx
@@ -1,8 +1,11 @@
import { useMergePreview } from '@/object-record/record-merge/hooks/useMergePreview';
-import { CardComponents } from '@/object-record/record-show/components/CardComponents';
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { getCardComponent } from '@/object-record/record-show/utils/getCardComponent';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { isDefined } from 'twenty-shared/utils';
import { Section } from 'twenty-ui/layout';
+import { PageLayoutType } from '~/generated/graphql';
type MergePreviewTabProps = {
objectNameSingular: string;
@@ -22,21 +25,27 @@ export const MergePreviewTab = ({
const recordId = mergePreviewRecord?.id ?? 'merge-preview-loading';
return (
-
+ targetObjectNameSingular: objectNameSingular,
+ },
+ layoutType: PageLayoutType.RECORD_PAGE,
+ isInRightDrawer: true,
+ }}
+ >
+
+
+
+ {getCardComponent(CardType.FieldCard, {
+ showDuplicatesSection: false,
+ })}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx
index a30f3b794c..761d450db0 100644
--- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx
@@ -1,6 +1,9 @@
-import { CardComponents } from '@/object-record/record-show/components/CardComponents';
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { getCardComponent } from '@/object-record/record-show/utils/getCardComponent';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { Section } from 'twenty-ui/layout';
+import { PageLayoutType } from '~/generated/graphql';
type MergeRecordTabProps = {
isInRightDrawer?: boolean;
@@ -13,21 +16,27 @@ export const MergeRecordTab = ({
recordId,
}: MergeRecordTabProps) => {
return (
-
+ targetObjectNameSingular: objectNameSingular,
+ },
+ layoutType: PageLayoutType.RECORD_PAGE,
+ isInRightDrawer: true,
+ }}
+ >
+
+
+
+ {getCardComponent(CardType.FieldCard, {
+ showDuplicatesSection: false,
+ })}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx
deleted file mode 100644
index bdcac6d7b4..0000000000
--- a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-import { Calendar } from '@/activities/calendar/components/Calendar';
-import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
-import { EmailThreads } from '@/activities/emails/components/EmailThreads';
-import { Attachments } from '@/activities/files/components/Attachments';
-import { Notes } from '@/activities/notes/components/Notes';
-import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
-import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
-import { DashboardRenderer } from '@/dashboards/components/DashboardRenderer';
-import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
-import { CardType } from '@/object-record/record-show/types/CardType';
-import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
-import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
-import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
-import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
-import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
-import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
-import { WorkflowRunVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowRunVisualizerComponentInstanceContext';
-import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { lazy, Suspense, useId } from 'react';
-import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
-
-const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
- background: ${({ theme, isInRightDrawer }) =>
- isInRightDrawer ? theme.background.secondary : ''};
- border: ${({ isInRightDrawer, theme }) =>
- isInRightDrawer ? `1px solid ${theme.border.color.medium}` : ''};
- border-radius: ${({ isInRightDrawer, theme }) =>
- isInRightDrawer ? theme.border.radius.md : ''};
- height: ${({ isInRightDrawer }) => (isInRightDrawer ? 'auto' : '100%')};
-
- margin: ${({ isInRightDrawer, theme }) =>
- isInRightDrawer ? theme.spacing(4) : ''};
-`;
-
-const StyledLoadingSkeletonContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: ${({ theme }) => theme.spacing(2)};
- height: 100%;
- padding: ${({ theme }) => theme.spacing(4)};
- width: 100%;
-`;
-
-type CardComponentProps = {
- targetableObject: Pick<
- ActivityTargetableObject,
- 'targetObjectNameSingular' | 'id'
- >;
- isInRightDrawer?: boolean;
-};
-
-type CardComponentType = (
- props: CardComponentProps | FieldsCardComponentProps,
-) => JSX.Element | null;
-
-type FieldsCardComponentProps = CardComponentProps & {
- showDuplicatesSection?: boolean;
-};
-
-const LoadingSkeleton = () => {
- const theme = useTheme();
-
- return (
-
-
-
-
-
-
-
- );
-};
-
-const WorkflowVisualizer = lazy(() =>
- import('@/workflow/workflow-diagram/components/WorkflowVisualizer').then(
- (module) => ({
- default: module.WorkflowVisualizer,
- }),
- ),
-);
-
-const WorkflowVersionVisualizer = lazy(() =>
- import(
- '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer'
- ).then((module) => ({
- default: module.WorkflowVersionVisualizer,
- })),
-);
-
-const WorkflowRunVisualizer = lazy(() =>
- import('@/workflow/workflow-diagram/components/WorkflowRunVisualizer').then(
- (module) => ({
- default: module.WorkflowRunVisualizer,
- }),
- ),
-);
-
-export const CardComponents: Record = {
- [CardType.TimelineCard]: ({ targetableObject, isInRightDrawer }) => (
-
- ),
-
- [CardType.FieldCard]: ({
- targetableObject,
- isInRightDrawer,
- showDuplicatesSection,
- }: FieldsCardComponentProps) => (
-
-
-
- ),
-
- [CardType.RichTextCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.TaskCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.NoteCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.FileCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.EmailCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.CalendarCard]: ({ targetableObject }) => (
-
- ),
-
- [CardType.WorkflowCard]: ({ targetableObject }) => {
- return (
-
-
- }>
-
-
-
- );
- },
-
- [CardType.WorkflowVersionCard]: ({ targetableObject }) => {
- return (
-
-
- }>
-
-
-
- );
- },
-
- [CardType.WorkflowRunCard]: ({ targetableObject }) => {
- const componentId = useId();
-
- return (
-
-
-
-
- }>
-
-
-
-
- );
- },
-
- [CardType.DashboardCard]: ({ targetableObject }) => {
- return ;
- },
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
index f360bf2723..61e66b3498 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
@@ -1,25 +1,22 @@
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
-import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
+import { type FieldCardConfiguration } from '@/object-record/record-show/types/CardConfiguration';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
type FieldsCardProps = {
- objectNameSingular: string;
- objectRecordId: string;
- showDuplicatesSection?: boolean;
+ configuration?: FieldCardConfiguration;
};
-export const FieldsCard = ({
- objectNameSingular,
- objectRecordId,
- showDuplicatesSection = true,
-}: FieldsCardProps) => {
- const { isInRightDrawer } = useIsInRightDrawerOrThrow();
+export const FieldsCard = ({ configuration }: FieldsCardProps) => {
+ const targetRecord = useTargetRecord();
+ const { isInRightDrawer } = useLayoutRenderingContext();
return (
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
index c610a5920e..9848032b88 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
@@ -3,25 +3,39 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
-import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/constants/BaseRecordLayout';
-import { CardType } from '@/object-record/record-show/types/CardType';
+import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/layouts/base-record-layout';
+import { COMPANY_RECORD_LAYOUT } from '@/object-record/record-show/layouts/company-record-layout';
+import { DASHBOARD_RECORD_LAYOUT } from '@/object-record/record-show/layouts/dashboard-record-layout';
+import { NOTE_RECORD_LAYOUT } from '@/object-record/record-show/layouts/note-record-layout';
+import { OPPORTUNITY_RECORD_LAYOUT } from '@/object-record/record-show/layouts/opportunity-record-layout';
+import { PERSON_RECORD_LAYOUT } from '@/object-record/record-show/layouts/person-record-layout';
+import { TASK_RECORD_LAYOUT } from '@/object-record/record-show/layouts/task-record-layout';
+import { WORKFLOW_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-record-layout';
+import { WORKFLOW_RUN_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-run-record-layout';
+import { WORKFLOW_VERSION_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-version-record-layout';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
-import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
+import { evaluateTabVisibility } from '@/object-record/record-show/utils/evaluateTabVisibility';
import { type RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
-import { isDefined } from 'twenty-shared/utils';
-import {
- IconCalendarEvent,
- IconHome,
- IconLayoutDashboard,
- IconMail,
- IconNotes,
- IconSettings,
-} from 'twenty-ui/display';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
+import { IconHome, useIcons } from 'twenty-ui/display';
+
+// Object-specific layouts that override or extend the base layout
+const OBJECT_SPECIFIC_LAYOUTS: Partial<
+ Record
+> = {
+ [CoreObjectNameSingular.Note]: NOTE_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Task]: TASK_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Company]: COMPANY_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Person]: PERSON_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Opportunity]: OPPORTUNITY_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Workflow]: WORKFLOW_RECORD_LAYOUT,
+ [CoreObjectNameSingular.WorkflowVersion]: WORKFLOW_VERSION_RECORD_LAYOUT,
+ [CoreObjectNameSingular.WorkflowRun]: WORKFLOW_RUN_RECORD_LAYOUT,
+ [CoreObjectNameSingular.Dashboard]: DASHBOARD_RECORD_LAYOUT,
+};
export const useRecordShowContainerTabs = (
loading: boolean,
@@ -34,257 +48,19 @@ export const useRecordShowContainerTabs = (
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
-
- // Object-specific layouts that override or extend the base layout
- const OBJECT_SPECIFIC_LAYOUTS: Partial<
- Record
- > = useMemo(
- () => ({
- [CoreObjectNameSingular.Note]: {
- tabs: {
- richText: {
- title: 'Note',
- position: 101,
- Icon: IconNotes,
- cards: [{ type: CardType.RichTextCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- tasks: null,
- notes: null,
- },
- },
- [CoreObjectNameSingular.Task]: {
- tabs: {
- richText: {
- title: 'Note',
- position: 101,
- Icon: IconNotes,
- cards: [{ type: CardType.RichTextCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- tasks: null,
- notes: null,
- },
- },
- [CoreObjectNameSingular.Company]: {
- tabs: {
- emails: {
- title: 'Emails',
- position: 600,
- Icon: IconMail,
- cards: [{ type: CardType.EmailCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- calendar: {
- title: 'Calendar',
- position: 700,
- Icon: IconCalendarEvent,
- cards: [{ type: CardType.CalendarCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- },
- },
- [CoreObjectNameSingular.Person]: {
- tabs: {
- emails: {
- title: 'Emails',
- position: 600,
- Icon: IconMail,
- cards: [{ type: CardType.EmailCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- calendar: {
- title: 'Calendar',
- position: 700,
- Icon: IconCalendarEvent,
- cards: [{ type: CardType.CalendarCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- },
- },
- [CoreObjectNameSingular.Opportunity]: {
- tabs: {
- emails: {
- title: 'Emails',
- position: 600,
- Icon: IconMail,
- cards: [{ type: CardType.EmailCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- calendar: {
- title: 'Calendar',
- position: 700,
- Icon: IconCalendarEvent,
- cards: [{ type: CardType.CalendarCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- },
- },
- [CoreObjectNameSingular.Workflow]: {
- hideSummaryAndFields: true,
- tabs: {
- workflow: {
- title: 'Flow',
- position: 101,
- Icon: IconSettings,
- cards: [{ type: CardType.WorkflowCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- timeline: null,
- fields: null,
- tasks: null,
- notes: null,
- files: null,
- },
- },
- [CoreObjectNameSingular.WorkflowVersion]: {
- tabs: {
- workflowVersion: {
- title: 'Flow',
- position: 101,
- Icon: IconSettings,
- cards: [{ type: CardType.WorkflowVersionCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- timeline: null,
- tasks: null,
- notes: null,
- files: null,
- },
- },
- [CoreObjectNameSingular.WorkflowRun]: {
- tabs: {
- workflowRun: {
- title: 'Flow',
- position: 101,
- Icon: IconSettings,
- cards: [{ type: CardType.WorkflowRunCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- timeline: null,
- tasks: null,
- notes: null,
- files: null,
- },
- },
- [CoreObjectNameSingular.Dashboard]: {
- hideSummaryAndFields: true,
- hideFieldsInSidePanel: true,
- tabs: {
- dashboard: {
- title: 'Dashboard',
- position: 101,
- Icon: IconLayoutDashboard,
- cards: [{ type: CardType.DashboardCard }],
- hide: {
- ifMobile: false,
- ifDesktop: false,
- ifInRightDrawer: false,
- ifFeaturesDisabled: [],
- ifRequiredObjectsInactive: [],
- ifRelationsMissing: [],
- },
- },
- timeline: null,
- tasks: null,
- notes: null,
- files: null,
- },
- },
- }),
- [],
- );
-
- const baseRecordLayout = BASE_RECORD_LAYOUT;
+ const { getIcon } = useIcons();
// Merge base layout with object-specific layout
const recordLayout: RecordLayout = useMemo(() => {
return {
- ...baseRecordLayout,
+ ...BASE_RECORD_LAYOUT,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
tabs: {
- ...baseRecordLayout.tabs,
+ ...BASE_RECORD_LAYOUT.tabs,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
},
};
- }, [OBJECT_SPECIFIC_LAYOUTS, baseRecordLayout, targetObjectNameSingular]);
+ }, [targetObjectNameSingular]);
return {
layout: recordLayout,
@@ -294,7 +70,9 @@ export const useRecordShowContainerTabs = (
entry[1] !== null && entry[1] !== undefined,
)
.sort(([, a], [, b]) => a.position - b.position)
- .map(([key, { title, Icon, hide, cards, targetObjectNameSingular }]) => {
+ .map(([key, { title, icon, hide, cards }]) => {
+ const Icon = getIcon(icon);
+
// Special handling for fields tab
if (key === 'fields') {
return {
@@ -308,62 +86,22 @@ export const useRecordShowContainerTabs = (
};
}
- const baseHide =
- (hide.ifMobile && isMobile) ||
- (hide.ifDesktop && !isMobile) ||
- (hide.ifInRightDrawer && isInRightDrawer);
-
- const featureNotEnabled =
- hide.ifFeaturesDisabled.length > 0 &&
- !hide.ifFeaturesDisabled.every((flagKey) => {
- return !!currentWorkspace?.featureFlags?.find(
- (flag) => flag.key === flagKey && flag.value,
- );
- });
-
- const targetObjectMetadataId = objectMetadataItems.find(
- (item) => item.nameSingular === targetObjectNameSingular,
- )?.id;
-
- const permissionHide =
- hide.ifNoReadPermission &&
- isDefined(targetObjectNameSingular) &&
- !getObjectPermissionsFromMapByObjectMetadataId({
- objectPermissionsByObjectMetadataId,
- objectMetadataId: targetObjectMetadataId ?? '',
- })?.canReadObjectRecords;
-
- const requiredObjectsInactive =
- hide.ifRequiredObjectsInactive.length > 0 &&
- !hide.ifRequiredObjectsInactive.every((obj) =>
- objectMetadataItems.some(
- (item) => item.nameSingular === obj && item.isActive,
- ),
- );
-
- const relationsDontExist =
- hide.ifRelationsMissing.length > 0 &&
- !hide.ifRelationsMissing.every((rel) =>
- objectMetadataItem.fields.some(
- (field) =>
- field.type === FieldMetadataType.RELATION &&
- field.name === rel &&
- field.isActive,
- ),
- );
+ // Use extracted visibility evaluation logic
+ const shouldHide = evaluateTabVisibility(hide, {
+ isMobile,
+ isInRightDrawer,
+ currentWorkspace,
+ objectMetadataItems,
+ objectPermissionsByObjectMetadataId,
+ targetObjectMetadataItem: objectMetadataItem,
+ });
return {
id: key,
title,
Icon,
cards,
- hide:
- loading ||
- baseHide ||
- featureNotEnabled ||
- requiredObjectsInactive ||
- relationsDontExist ||
- permissionHide,
+ hide: loading || shouldHide,
};
})
// When isInRightDrawer === true, we merge first and second tab into first tab
diff --git a/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/base-record-layout.ts
similarity index 85%
rename from packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts
rename to packages/twenty-front/src/modules/object-record/record-show/layouts/base-record-layout.ts
index fb95cb3017..a0beb6ad42 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/base-record-layout.ts
@@ -1,19 +1,12 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
-import {
- IconCheckbox,
- IconList,
- IconNotes,
- IconPaperclip,
- IconTimelineEvent,
-} from 'twenty-ui/display';
export const BASE_RECORD_LAYOUT: RecordLayout = {
tabs: {
fields: {
title: 'Fields',
- Icon: IconList,
+ icon: 'IconList',
position: 100,
cards: [{ type: CardType.FieldCard }],
hide: {
@@ -27,7 +20,7 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
},
timeline: {
title: 'Timeline',
- Icon: IconTimelineEvent,
+ icon: 'IconTimelineEvent',
position: 200,
cards: [{ type: CardType.TimelineCard }],
hide: {
@@ -41,10 +34,9 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
},
tasks: {
title: 'Tasks',
- Icon: IconCheckbox,
+ icon: 'IconCheckbox',
position: 300,
cards: [{ type: CardType.TaskCard }],
- targetObjectNameSingular: CoreObjectNameSingular.Task,
hide: {
ifMobile: false,
ifDesktop: false,
@@ -53,14 +45,14 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
ifRequiredObjectsInactive: [CoreObjectNameSingular.Task],
ifRelationsMissing: ['taskTargets'],
ifNoReadPermission: true,
+ ifNoReadPermissionObject: CoreObjectNameSingular.Task,
},
},
notes: {
title: 'Notes',
- Icon: IconNotes,
+ icon: 'IconNotes',
position: 400,
cards: [{ type: CardType.NoteCard }],
- targetObjectNameSingular: CoreObjectNameSingular.Note,
hide: {
ifMobile: false,
ifDesktop: false,
@@ -69,11 +61,12 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
ifRequiredObjectsInactive: [CoreObjectNameSingular.Note],
ifRelationsMissing: ['noteTargets'],
ifNoReadPermission: true,
+ ifNoReadPermissionObject: CoreObjectNameSingular.Note,
},
},
files: {
title: 'Files',
- Icon: IconPaperclip,
+ icon: 'IconPaperclip',
position: 500,
cards: [{ type: CardType.FileCard }],
hide: {
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/company-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/company-record-layout.ts
new file mode 100644
index 0000000000..ca11130b6e
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/company-record-layout.ts
@@ -0,0 +1,35 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const COMPANY_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ emails: {
+ title: 'Emails',
+ position: 600,
+ icon: 'IconMail',
+ cards: [{ type: CardType.EmailCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ calendar: {
+ title: 'Calendar',
+ position: 700,
+ icon: 'IconCalendarEvent',
+ cards: [{ type: CardType.CalendarCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/dashboard-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/dashboard-record-layout.ts
new file mode 100644
index 0000000000..de8d330549
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/dashboard-record-layout.ts
@@ -0,0 +1,27 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const DASHBOARD_RECORD_LAYOUT: RecordLayout = {
+ hideSummaryAndFields: true,
+ hideFieldsInSidePanel: true,
+ tabs: {
+ dashboard: {
+ title: 'Dashboard',
+ position: 101,
+ icon: 'IconLayoutDashboard',
+ cards: [{ type: CardType.DashboardCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ timeline: null,
+ tasks: null,
+ notes: null,
+ files: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts
new file mode 100644
index 0000000000..fde98f3a9d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/note-record-layout.ts
@@ -0,0 +1,23 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const NOTE_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ richText: {
+ title: 'Note',
+ position: 101,
+ icon: 'IconNotes',
+ cards: [{ type: CardType.RichTextCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ tasks: null,
+ notes: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/opportunity-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/opportunity-record-layout.ts
new file mode 100644
index 0000000000..ab6157c172
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/opportunity-record-layout.ts
@@ -0,0 +1,35 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const OPPORTUNITY_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ emails: {
+ title: 'Emails',
+ position: 600,
+ icon: 'IconMail',
+ cards: [{ type: CardType.EmailCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ calendar: {
+ title: 'Calendar',
+ position: 700,
+ icon: 'IconCalendarEvent',
+ cards: [{ type: CardType.CalendarCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/person-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/person-record-layout.ts
new file mode 100644
index 0000000000..fc18e172d0
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/person-record-layout.ts
@@ -0,0 +1,35 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const PERSON_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ emails: {
+ title: 'Emails',
+ position: 600,
+ icon: 'IconMail',
+ cards: [{ type: CardType.EmailCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ calendar: {
+ title: 'Calendar',
+ position: 700,
+ icon: 'IconCalendarEvent',
+ cards: [{ type: CardType.CalendarCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts
new file mode 100644
index 0000000000..3c0579b934
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/task-record-layout.ts
@@ -0,0 +1,23 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const TASK_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ richText: {
+ title: 'Note',
+ position: 101,
+ icon: 'IconNotes',
+ cards: [{ type: CardType.RichTextCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ tasks: null,
+ notes: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-record-layout.ts
new file mode 100644
index 0000000000..d730896e78
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-record-layout.ts
@@ -0,0 +1,27 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const WORKFLOW_RECORD_LAYOUT: RecordLayout = {
+ hideSummaryAndFields: true,
+ tabs: {
+ workflow: {
+ title: 'Flow',
+ position: 101,
+ icon: 'IconSettings',
+ cards: [{ type: CardType.WorkflowCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ timeline: null,
+ fields: null,
+ tasks: null,
+ notes: null,
+ files: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-run-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-run-record-layout.ts
new file mode 100644
index 0000000000..d4ae735dc9
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-run-record-layout.ts
@@ -0,0 +1,25 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const WORKFLOW_RUN_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ workflowRun: {
+ title: 'Flow',
+ position: 101,
+ icon: 'IconSettings',
+ cards: [{ type: CardType.WorkflowRunCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ timeline: null,
+ tasks: null,
+ notes: null,
+ files: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-version-record-layout.ts b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-version-record-layout.ts
new file mode 100644
index 0000000000..241d1ca732
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/layouts/workflow-version-record-layout.ts
@@ -0,0 +1,25 @@
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+
+export const WORKFLOW_VERSION_RECORD_LAYOUT: RecordLayout = {
+ tabs: {
+ workflowVersion: {
+ title: 'Flow',
+ position: 101,
+ icon: 'IconSettings',
+ cards: [{ type: CardType.WorkflowVersionCard }],
+ hide: {
+ ifMobile: false,
+ ifDesktop: false,
+ ifInRightDrawer: false,
+ ifFeaturesDisabled: [],
+ ifRequiredObjectsInactive: [],
+ ifRelationsMissing: [],
+ },
+ },
+ timeline: null,
+ tasks: null,
+ notes: null,
+ files: null,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts b/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts
new file mode 100644
index 0000000000..a61fde86ee
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/types/CardConfiguration.ts
@@ -0,0 +1,29 @@
+import { type CardType } from '@/object-record/record-show/types/CardType';
+
+// Card configuration types - each card type can define its own configuration
+export type FieldCardConfiguration = {
+ showDuplicatesSection?: boolean;
+};
+
+// For cards that don't need configuration, use undefined
+export type EmptyCardConfiguration = undefined;
+
+// Type mapping from CardType to its specific configuration type
+// This creates precise typing: each CardType is linked to exactly one configuration type
+export type CardTypeToConfiguration = {
+ [CardType.FieldCard]: FieldCardConfiguration;
+ [CardType.TimelineCard]: EmptyCardConfiguration;
+ [CardType.TaskCard]: EmptyCardConfiguration;
+ [CardType.NoteCard]: EmptyCardConfiguration;
+ [CardType.FileCard]: EmptyCardConfiguration;
+ [CardType.EmailCard]: EmptyCardConfiguration;
+ [CardType.CalendarCard]: EmptyCardConfiguration;
+ [CardType.RichTextCard]: EmptyCardConfiguration;
+ [CardType.WorkflowCard]: EmptyCardConfiguration;
+ [CardType.WorkflowVersionCard]: EmptyCardConfiguration;
+ [CardType.WorkflowRunCard]: EmptyCardConfiguration;
+ [CardType.DashboardCard]: EmptyCardConfiguration;
+};
+
+// Union type for all card configurations (for general use)
+export type CardConfiguration = FieldCardConfiguration | EmptyCardConfiguration;
diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/evaluateTabVisibility.ts b/packages/twenty-front/src/modules/object-record/record-show/utils/evaluateTabVisibility.ts
new file mode 100644
index 0000000000..f4a6365f35
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/utils/evaluateTabVisibility.ts
@@ -0,0 +1,109 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
+import { type TabVisibilityConfig } from '@/ui/layout/tab-list/types/TabVisibilityConfig';
+import { type ObjectPermissions } from 'twenty-shared/types';
+import { FieldMetadataType, type FeatureFlagDto } from '~/generated/graphql';
+
+export type TabVisibilityContext = {
+ isMobile: boolean;
+ isInRightDrawer: boolean;
+ currentWorkspace: {
+ featureFlags?: FeatureFlagDto[] | null;
+ } | null;
+ objectMetadataItems: ObjectMetadataItem[];
+ objectPermissionsByObjectMetadataId: Record<
+ string,
+ ObjectPermissions & { objectMetadataId: string }
+ >;
+ targetObjectMetadataItem: ObjectMetadataItem;
+};
+
+export const evaluateTabVisibility = (
+ hide: TabVisibilityConfig,
+ context: TabVisibilityContext,
+): boolean => {
+ const {
+ isMobile,
+ isInRightDrawer,
+ currentWorkspace,
+ objectMetadataItems,
+ objectPermissionsByObjectMetadataId,
+ } = context;
+
+ const baseHide =
+ (hide.ifMobile && isMobile) ||
+ (hide.ifDesktop && !isMobile) ||
+ (hide.ifInRightDrawer && isInRightDrawer);
+
+ if (baseHide) {
+ return true;
+ }
+
+ const featureNotEnabled =
+ hide.ifFeaturesDisabled.length > 0 &&
+ !hide.ifFeaturesDisabled.every((flagKey) => {
+ const featureFlags = currentWorkspace?.featureFlags;
+ if (!featureFlags) {
+ return false;
+ }
+ return !!featureFlags.find((flag) => flag.key === flagKey && flag.value);
+ });
+
+ if (featureNotEnabled) {
+ return true;
+ }
+
+ const requiredObjectInactive = hide.ifRequiredObjectsInactive.some(
+ (requiredObjectName) => {
+ const objectMetadataItem = objectMetadataItems.find(
+ (item) => item.nameSingular === requiredObjectName,
+ );
+ return !objectMetadataItem?.isActive;
+ },
+ );
+
+ if (requiredObjectInactive) {
+ return true;
+ }
+
+ const relationMissing = hide.ifRelationsMissing.some((relationName) => {
+ return !context.targetObjectMetadataItem.fields.some(
+ (field) =>
+ field.name === relationName &&
+ field.type === FieldMetadataType.RELATION &&
+ field.isActive === true,
+ );
+ });
+
+ if (relationMissing) {
+ return true;
+ }
+
+ const noReadPermission =
+ hide.ifNoReadPermission === true &&
+ isDefined(hide.ifNoReadPermissionObject) &&
+ (() => {
+ const targetObjectMetadataId = objectMetadataItems.find(
+ (item) => item.nameSingular === hide.ifNoReadPermissionObject,
+ )?.id;
+
+ if (!isDefined(targetObjectMetadataId)) {
+ return false;
+ }
+
+ const objectPermissions = getObjectPermissionsForObject(
+ objectPermissionsByObjectMetadataId,
+ targetObjectMetadataId,
+ );
+
+ return objectPermissions.canReadObjectRecords === false;
+ })();
+
+ if (noReadPermission) {
+ return true;
+ }
+
+ return false;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx
new file mode 100644
index 0000000000..ef629da36d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx
@@ -0,0 +1,94 @@
+import { CalendarEventsCard } from '@/activities/calendar/components/CalendarEventsCard';
+import { EmailsCard } from '@/activities/emails/components/EmailsCard';
+import { FilesCard } from '@/activities/files/components/FilesCard';
+import { NotesCard } from '@/activities/notes/components/NotesCard';
+import { TasksCard } from '@/activities/tasks/components/TasksCard';
+import { TimelineCard } from '@/activities/timeline-activities/components/TimelineCard';
+import { DashboardCard } from '@/dashboards/components/DashboardCard';
+import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
+import {
+ type CardConfiguration,
+ type CardTypeToConfiguration,
+ type FieldCardConfiguration,
+} from '@/object-record/record-show/types/CardConfiguration';
+import { CardType } from '@/object-record/record-show/types/CardType';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { RichTextCard } from '@/ui/layout/show-page/components/RichTextCard';
+import { WorkflowCard } from '@/workflow/workflow-diagram/components/WorkflowCard';
+import { WorkflowRunCard } from '@/workflow/workflow-diagram/components/WorkflowRunCard';
+import { WorkflowVersionCard } from '@/workflow/workflow-diagram/components/WorkflowVersionCard';
+import { assertUnreachable } from 'twenty-shared/utils';
+
+const CardRenderer = ({
+ Component,
+ configuration,
+}: {
+ Component: React.ComponentType<{ configuration?: T }> | React.ComponentType;
+ configuration?: T;
+}) => {
+ const { targetRecord } = useLayoutRenderingContext();
+
+ if (!targetRecord) {
+ return null;
+ }
+
+ // TypeScript can't infer if Component accepts configuration prop or not
+ // So we cast to the more permissive type and let the component ignore unused props
+ const ComponentWithConfig = Component as React.ComponentType<{
+ configuration?: T;
+ }>;
+
+ return ;
+};
+
+// Generic function with precise type mapping from CardType to Configuration
+// TypeScript will enforce that the correct configuration type is passed for each card type
+export const getCardComponent = (
+ cardType: T,
+ configuration?: CardTypeToConfiguration[T],
+): JSX.Element | null => {
+ switch (cardType) {
+ case CardType.TimelineCard:
+ return ;
+
+ case CardType.FieldCard:
+ return (
+
+ );
+
+ case CardType.RichTextCard:
+ return ;
+
+ case CardType.TaskCard:
+ return ;
+
+ case CardType.NoteCard:
+ return ;
+
+ case CardType.FileCard:
+ return ;
+
+ case CardType.EmailCard:
+ return ;
+
+ case CardType.CalendarCard:
+ return ;
+
+ case CardType.WorkflowCard:
+ return ;
+
+ case CardType.WorkflowVersionCard:
+ return ;
+
+ case CardType.WorkflowRunCard:
+ return ;
+
+ case CardType.DashboardCard:
+ return ;
+ default:
+ assertUnreachable(cardType);
+ }
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
index aed9146826..ed56896b04 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
@@ -4,9 +4,11 @@ import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRe
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
+import { PageLayoutType } from '~/generated/graphql';
type PageLayoutRendererProps = {
pageLayoutId: string;
@@ -18,22 +20,30 @@ export const PageLayoutRenderer = ({
onInitialized,
}: PageLayoutRendererProps) => {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts
index 7aa22e51fd..058a351aeb 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts
@@ -2,12 +2,13 @@ import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objec
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
export const useCompanyDefaultChartConfig = () => {
const companyObjectMetadata = useRecoilValue(
objectMetadataItemFamilySelector({
- objectName: 'company',
+ objectName: CoreObjectNameSingular.Company,
objectNameType: 'singular',
}),
);
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
index 6ab65abecf..604f95e3d1 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
@@ -1,6 +1,7 @@
import { useFormContext } from 'react-hook-form';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
import {
@@ -82,7 +83,8 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
}}
shrink
objectNameSingulars={[
- relationObjectMetadataItem?.nameSingular ?? 'company',
+ relationObjectMetadataItem?.nameSingular ??
+ CoreObjectNameSingular.Company,
]}
fieldPreviewTargetObjectNameSingular={objectNameSingular}
pluralizeLabel={oppositeRelationType === RelationType.ONE_TO_MANY}
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx
index ee4cfe1d25..90616b2169 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx
@@ -2,6 +2,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
@@ -75,7 +76,7 @@ export const SettingsDataModelRelationFieldPreview = ({
const metadata = {
fieldName,
- objectMetadataNameSingular: 'company',
+ objectMetadataNameSingular: CoreObjectNameSingular.Company,
relationObjectMetadataNameSingular: relationTargetObjectNameSingular,
options: [],
settings: fieldMetadataItem.settings,
diff --git a/packages/twenty-front/src/modules/ui/layout/contexts/LayoutRenderingContext.tsx b/packages/twenty-front/src/modules/ui/layout/contexts/LayoutRenderingContext.tsx
new file mode 100644
index 0000000000..104c404e97
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/contexts/LayoutRenderingContext.tsx
@@ -0,0 +1,20 @@
+import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { type PageLayoutType } from '~/generated/graphql';
+import { createRequiredContext } from '~/utils/createRequiredContext';
+
+export type LayoutRenderingContextType = {
+ // Optional target record - only present for record pages that display data about a specific record
+ // Undefined for dashboards which are standalone
+ // Uses ActivityTargetableObject shape for compatibility with existing components
+ targetRecord?: Pick<
+ ActivityTargetableObject,
+ 'id' | 'targetObjectNameSingular'
+ >;
+
+ layoutType: PageLayoutType;
+
+ isInRightDrawer: boolean;
+};
+
+export const [LayoutRenderingProvider, useLayoutRenderingContext] =
+ createRequiredContext('LayoutRenderingContext');
diff --git a/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts b/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts
new file mode 100644
index 0000000000..7c6c8ace34
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts
@@ -0,0 +1,13 @@
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+
+export const useTargetRecord = () => {
+ const { targetRecord } = useLayoutRenderingContext();
+
+ if (!targetRecord) {
+ throw new Error(
+ 'useTargetRecord must be used within a record page context (targetRecord is required)',
+ );
+ }
+
+ return targetRecord;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx
similarity index 78%
rename from packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx
rename to packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx
index eaa15528c1..ecb51787de 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/RichTextCard.tsx
@@ -1,7 +1,7 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
-import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -46,38 +46,31 @@ const LoadingSkeleton = () => {
);
};
-export const ShowPageActivityContainer = ({
- targetableObject,
-}: {
- targetableObject: Pick<
- ActivityTargetableObject,
- 'targetObjectNameSingular' | 'id'
- >;
-}) => {
- const activityObjectNameSingular =
- targetableObject.targetObjectNameSingular as
- | CoreObjectNameSingular.Note
- | CoreObjectNameSingular.Task;
-
+export const RichTextCard = () => {
+ const targetRecord = useTargetRecord();
const activityBodyV2 = useRecoilValue(
recordStoreFamilySelector({
- recordId: targetableObject.id,
+ recordId: targetRecord.id,
fieldName: 'bodyV2',
}),
);
+ const activityObjectNameSingular = targetRecord.targetObjectNameSingular as
+ | CoreObjectNameSingular.Note
+ | CoreObjectNameSingular.Task;
+
if (!isDefined(activityBodyV2)) {
return ;
}
return (
}>
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx
index 4b10005a21..d5b913f533 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx
@@ -2,14 +2,16 @@ import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/Record
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
-import { CardComponents } from '@/object-record/record-show/components/CardComponents';
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
+import { getCardComponent } from '@/object-record/record-show/utils/getCardComponent';
+import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
+import { PageLayoutType } from '~/generated/graphql';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
@@ -18,6 +20,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
+import React from 'react';
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
display: flex;
@@ -92,27 +95,21 @@ export const ShowPageSubContainer = ({
/>
);
- const fieldsCard = (
-
- );
+ const fieldsCard = ;
const renderActiveTabContent = () => {
const activeTab = tabs.find((tab) => tab.id === activeTabId);
if (!activeTab?.cards?.length) return null;
- return activeTab.cards.map((card, index) => {
- const CardComponent = CardComponents[card.type];
- return CardComponent ? (
-
- ) : null;
- });
+ return (
+ <>
+ {activeTab.cards.map((card, index) => (
+
+ {getCardComponent(card.type, card.configuration)}
+
+ ))}
+ >
+ );
};
const visibleTabs = tabs.filter((tab) => !tab.hide);
@@ -121,41 +118,52 @@ export const ShowPageSubContainer = ({
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
return (
-
- {displaySummaryAndFields && (
-
- {summaryCard}
- {fieldsCard}
-
- )}
-
- 1}>
-
-
- {(isMobile || isInRightDrawer) && summaryCard}
-
- {renderActiveTabContent()}
-
- {isInRightDrawer && (
- ,
- ,
- ]}
- />
+
+ {displaySummaryAndFields && (
+
+ {summaryCard}
+ {fieldsCard}
+
)}
-
-
+
+ 1}>
+
+
+ {(isMobile || isInRightDrawer) && summaryCard}
+
+ {renderActiveTabContent()}
+
+ {isInRightDrawer && (
+ ,
+ ,
+ ]}
+ />
+ )}
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/LayoutCard.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/LayoutCard.ts
index 1778677b19..ffda093ce9 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/LayoutCard.ts
+++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/LayoutCard.ts
@@ -1,5 +1,7 @@
+import { type CardConfiguration } from '@/object-record/record-show/types/CardConfiguration';
import { type CardType } from '@/object-record/record-show/types/CardType';
export type LayoutCard = {
type: CardType;
+ configuration?: CardConfiguration;
};
diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
index 45256f358c..9a061cdf90 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
+++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
@@ -1,12 +1,10 @@
import { type LayoutCard } from '@/ui/layout/tab-list/types/LayoutCard';
import { type TabVisibilityConfig } from '@/ui/layout/tab-list/types/TabVisibilityConfig';
-import { type IconComponent } from 'twenty-ui/display';
export type RecordLayoutTab = {
title: string;
position: number;
- Icon: IconComponent;
+ icon: string;
hide: TabVisibilityConfig;
cards: LayoutCard[];
- targetObjectNameSingular?: string;
};
diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
index 9d9d57e428..3b553174f0 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
+++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
@@ -9,4 +9,5 @@ export type TabVisibilityConfig = {
ifRequiredObjectsInactive: CoreObjectNameSingular[];
ifRelationsMissing: string[];
ifNoReadPermission?: boolean;
+ ifNoReadPermissionObject?: CoreObjectNameSingular;
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx
new file mode 100644
index 0000000000..d7c6646b52
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx
@@ -0,0 +1,24 @@
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
+import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
+import { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable';
+import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect';
+import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
+import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
+
+export const WorkflowCard = () => {
+ const targetRecord = useTargetRecord();
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunCard.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunCard.tsx
new file mode 100644
index 0000000000..cd2653a833
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunCard.tsx
@@ -0,0 +1,70 @@
+import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
+import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
+import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
+import { WorkflowRunVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizer';
+import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
+import { WorkflowRunVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowRunVisualizerComponentInstanceContext';
+import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { Suspense, useId } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+
+const StyledLoadingSkeletonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+ height: 100%;
+ padding: ${({ theme }) => theme.spacing(4)};
+ width: 100%;
+`;
+
+const LoadingSkeleton = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const WorkflowRunCard = () => {
+ const targetRecord = useTargetRecord();
+ const componentId = useId();
+
+ return (
+
+
+
+
+ }>
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionCard.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionCard.tsx
new file mode 100644
index 0000000000..523321be3e
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionCard.tsx
@@ -0,0 +1,56 @@
+import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
+import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
+import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
+import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
+import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { Suspense } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+
+const StyledLoadingSkeletonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+ height: 100%;
+ padding: ${({ theme }) => theme.spacing(4)};
+ width: 100%;
+`;
+
+const LoadingSkeleton = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const WorkflowVersionCard = () => {
+ const targetRecord = useTargetRecord();
+
+ return (
+
+
+ }>
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings.ts
index 4ac5f9dedf..ad1c5d54d4 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings.ts
@@ -1,3 +1,4 @@
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type WorkflowFormFieldType } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormFieldType';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
@@ -33,7 +34,7 @@ export const getDefaultFormFieldSettings = (type: WorkflowFormFieldType) => {
label: 'Record',
placeholder: `Select a Company`,
settings: {
- objectName: 'company',
+ objectName: CoreObjectNameSingular.Company,
},
};
case FieldMetadataType.SELECT: