Use record page layouts in the merge records feature (#18961)
There was a design issue. It revealed that we weren't using record page layouts in the merge records feature. ## Before <img width="802" height="1283" alt="image" src="https://github.com/user-attachments/assets/e4238144-e10e-47a6-83e6-7cc03ca89b15" /> ## After https://github.com/user-attachments/assets/c0fa9cf5-2b28-4696-bf20-8271dce9e62c
This commit is contained in:
committed by
GitHub
parent
d126d54bbc
commit
cfefe9273b
+13
-16
@@ -1,10 +1,8 @@
|
||||
import { usePerformMergePreview } from '@/object-record/record-merge/hooks/usePerformMergePreview';
|
||||
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 { PageLayoutSingleTabRenderer } from '@/page-layout/components/PageLayoutSingleTabRenderer';
|
||||
import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdForRecord';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
type MergePreviewTabProps = {
|
||||
@@ -18,7 +16,16 @@ export const MergePreviewTab = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (!isDefined(mergePreviewRecord) || isGeneratingPreview) {
|
||||
const { pageLayoutId } = usePageLayoutIdForRecord({
|
||||
id: mergePreviewRecord?.id ?? '',
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(mergePreviewRecord) ||
|
||||
isGeneratingPreview ||
|
||||
!isDefined(pageLayoutId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -35,17 +42,7 @@ export const MergePreviewTab = ({
|
||||
isInSidePanel: true,
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<SummaryCard
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={recordId}
|
||||
isInSidePanel={true}
|
||||
/>
|
||||
|
||||
{getCardComponent(CardType.FieldCard, {
|
||||
showDuplicatesSection: false,
|
||||
})}
|
||||
</Section>
|
||||
<PageLayoutSingleTabRenderer pageLayoutId={pageLayoutId} />
|
||||
</LayoutRenderingProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+16
-16
@@ -1,12 +1,11 @@
|
||||
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 { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
|
||||
import { PageLayoutSingleTabRenderer } from '@/page-layout/components/PageLayoutSingleTabRenderer';
|
||||
import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdForRecord';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
type MergeRecordTabProps = {
|
||||
isInSidePanel?: boolean;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
};
|
||||
@@ -15,6 +14,11 @@ export const MergeRecordTab = ({
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
}: MergeRecordTabProps) => {
|
||||
const { pageLayoutId } = usePageLayoutIdForRecord({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
@@ -26,17 +30,13 @@ export const MergeRecordTab = ({
|
||||
isInSidePanel: true,
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<SummaryCard
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={recordId}
|
||||
isInSidePanel={true}
|
||||
/>
|
||||
|
||||
{getCardComponent(CardType.FieldCard, {
|
||||
showDuplicatesSection: false,
|
||||
})}
|
||||
</Section>
|
||||
<RecordShowEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordId={recordId}
|
||||
/>
|
||||
{isDefined(pageLayoutId) && (
|
||||
<PageLayoutSingleTabRenderer pageLayoutId={pageLayoutId} />
|
||||
)}
|
||||
</LayoutRenderingProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-3
@@ -8,11 +8,11 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
|
||||
import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext';
|
||||
import { useMergeRecordsContainerTabs } from '@/object-record/record-merge/hooks/useMergeRecordsContainerTabs';
|
||||
import { useMergeRecordsSelectedRecords } from '@/object-record/record-merge/hooks/useMergeRecordsSelectedRecords';
|
||||
import { MergeRecordsTabId } from '@/object-record/record-merge/types/MergeRecordsTabId';
|
||||
import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useMergeRecordsContainerTabs } from '@/object-record/record-merge/hooks/useMergeRecordsContainerTabs';
|
||||
import { MergePreviewTab } from './MergePreviewTab';
|
||||
import { MergeRecordTab } from './MergeRecordTab';
|
||||
import { MergeRecordsFooter } from './MergeRecordsFooter';
|
||||
@@ -36,7 +36,6 @@ const StyledContentContainer = styled.div`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: ${themeCssVariables.spacing[16]};
|
||||
`;
|
||||
|
||||
type MergeRecordsContainerProps = {
|
||||
|
||||
+5
-5
@@ -1,15 +1,15 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMergeRecordsSelectedRecords } from '@/object-record/record-merge/hooks/useMergeRecordsSelectedRecords';
|
||||
import { useMergeRecordsSettings } from '@/object-record/record-merge/hooks/useMergeRecordsSettings';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { getPositionNumberIcon } from '@/object-record/record-merge/utils/getPositionNumberIcon';
|
||||
import { getPositionWordLabel } from '@/object-record/record-merge/utils/getPositionWordLabel';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSectionContainer = styled.div`
|
||||
margin: ${themeCssVariables.spacing[4]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
|
||||
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
|
||||
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 = {
|
||||
configuration?: FieldCardConfiguration;
|
||||
};
|
||||
|
||||
export const FieldsCard = ({ configuration }: FieldsCardProps) => {
|
||||
const targetRecord = useTargetRecord();
|
||||
const { isInSidePanel } = useLayoutRenderingContext();
|
||||
|
||||
const instanceId = `fields-card-${targetRecord.id}${isInSidePanel ? '-side-panel' : ''}`;
|
||||
|
||||
return (
|
||||
<RecordFieldsScopeContextProvider value={{ scopeInstanceId: instanceId }}>
|
||||
<RecordFieldList
|
||||
instanceId={instanceId}
|
||||
objectNameSingular={targetRecord.targetObjectNameSingular}
|
||||
objectRecordId={targetRecord.id}
|
||||
showDuplicatesSection={configuration?.showDuplicatesSection ?? true}
|
||||
/>
|
||||
</RecordFieldsScopeContextProvider>
|
||||
);
|
||||
};
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
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.FieldRichTextCard]: 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;
|
||||
@@ -1,14 +0,0 @@
|
||||
export enum CardType {
|
||||
FieldCard = 'FieldCard',
|
||||
TimelineCard = 'TimelineCard',
|
||||
TaskCard = 'TaskCard',
|
||||
NoteCard = 'NoteCard',
|
||||
FileCard = 'FileCard',
|
||||
EmailCard = 'EmailCard',
|
||||
CalendarCard = 'CalendarCard',
|
||||
WorkflowCard = 'WorkflowCard',
|
||||
WorkflowVersionCard = 'WorkflowVersionCard',
|
||||
WorkflowRunCard = 'WorkflowRunCard',
|
||||
FieldRichTextCard = 'FieldRichTextCard',
|
||||
DashboardCard = 'DashboardCard',
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
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 { 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 { FieldRichTextCard } from '@/ui/layout/show-page/components/FieldRichTextCard';
|
||||
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 = <T extends CardConfiguration>({
|
||||
Component,
|
||||
configuration,
|
||||
}: {
|
||||
Component: React.ComponentType<{ configuration?: T }> | React.ComponentType;
|
||||
configuration?: T;
|
||||
}) => {
|
||||
const { targetRecordIdentifier } = useLayoutRenderingContext();
|
||||
|
||||
if (!targetRecordIdentifier) {
|
||||
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 <ComponentWithConfig configuration={configuration} />;
|
||||
};
|
||||
|
||||
// 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 = <T extends CardType>(
|
||||
cardType: T,
|
||||
configuration?: CardTypeToConfiguration[T],
|
||||
): JSX.Element | null => {
|
||||
switch (cardType) {
|
||||
case CardType.TimelineCard:
|
||||
return <CardRenderer Component={TimelineCard} />;
|
||||
|
||||
case CardType.FieldCard:
|
||||
return (
|
||||
<CardRenderer
|
||||
Component={FieldsCard}
|
||||
configuration={configuration as FieldCardConfiguration | undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
case CardType.FieldRichTextCard:
|
||||
return <CardRenderer Component={FieldRichTextCard} />;
|
||||
|
||||
case CardType.TaskCard:
|
||||
return <CardRenderer Component={TasksCard} />;
|
||||
|
||||
case CardType.NoteCard:
|
||||
return <CardRenderer Component={NotesCard} />;
|
||||
|
||||
case CardType.FileCard:
|
||||
return <CardRenderer Component={FilesCard} />;
|
||||
|
||||
case CardType.EmailCard:
|
||||
return <CardRenderer Component={EmailsCard} />;
|
||||
|
||||
case CardType.CalendarCard:
|
||||
return <CardRenderer Component={CalendarEventsCard} />;
|
||||
|
||||
case CardType.WorkflowCard:
|
||||
return <CardRenderer Component={WorkflowCard} />;
|
||||
|
||||
case CardType.WorkflowVersionCard:
|
||||
return <CardRenderer Component={WorkflowVersionCard} />;
|
||||
|
||||
case CardType.WorkflowRunCard:
|
||||
return <CardRenderer Component={WorkflowRunCard} />;
|
||||
|
||||
case CardType.DashboardCard:
|
||||
throw new Error('Dashboard are handled separately currently');
|
||||
|
||||
default:
|
||||
assertUnreachable(cardType);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user