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);
|
||||
}
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
|
||||
import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
|
||||
import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
|
||||
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
|
||||
import { PageLayoutRecordPageCustomizationSessionRegistrationEffect } from '@/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect';
|
||||
import { PageLayoutRelationWidgetsSyncEffect } from '@/page-layout/components/PageLayoutRelationWidgetsSyncEffect';
|
||||
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
|
||||
import { getTabLayoutMode } from '@/page-layout/utils/getTabLayoutMode';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
type PageLayoutSingleTabRendererProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
const PageLayoutSingleTabRendererContent = () => {
|
||||
const pageLayoutIsInitialized = useAtomComponentStateValue(
|
||||
pageLayoutIsInitializedComponentState,
|
||||
);
|
||||
|
||||
if (!pageLayoutIsInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PageLayoutSingleTabRendererInner />;
|
||||
};
|
||||
|
||||
const PageLayoutSingleTabRendererInner = () => {
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
const targetRecordIdentifier = useTargetRecord();
|
||||
const { isInSidePanel } = useLayoutRenderingContext();
|
||||
|
||||
const sortedTabs = sortTabsByPosition(currentPageLayout.tabs);
|
||||
const firstTab = sortedTabs[0];
|
||||
|
||||
const firstTabWithVisibleWidgets = usePageLayoutTabWithVisibleWidgetsOrThrow(
|
||||
firstTab.id,
|
||||
);
|
||||
|
||||
const layoutMode = getTabLayoutMode({
|
||||
tab: firstTabWithVisibleWidgets,
|
||||
pageLayoutType: currentPageLayout.type,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SummaryCard
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
objectRecordId={targetRecordIdentifier.id}
|
||||
isInSidePanel={isInSidePanel}
|
||||
/>
|
||||
|
||||
<PageLayoutContentProvider
|
||||
value={{
|
||||
tabId: firstTab.id,
|
||||
layoutMode,
|
||||
}}
|
||||
>
|
||||
<PageLayoutContent />
|
||||
</PageLayoutContentProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageLayoutSingleTabRenderer = ({
|
||||
pageLayoutId,
|
||||
}: PageLayoutSingleTabRendererProps) => {
|
||||
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
|
||||
pageLayoutId,
|
||||
layoutType,
|
||||
targetRecordIdentifier,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLayoutComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: pageLayoutId,
|
||||
}}
|
||||
>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: tabListInstanceId,
|
||||
}}
|
||||
>
|
||||
<PageLayoutEditModeProvider
|
||||
layoutType={layoutType}
|
||||
pageLayoutId={pageLayoutId}
|
||||
>
|
||||
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
|
||||
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
|
||||
{!isRecordPageLayoutEditingEnabled && (
|
||||
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
|
||||
)}
|
||||
<PageLayoutSingleTabRendererContent />
|
||||
</PageLayoutEditModeProvider>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
</PageLayoutComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type LayoutCard } from '@/ui/layout/tab-list/types/LayoutCard';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type SingleTabProps<T extends string = string> = {
|
||||
@@ -8,6 +7,5 @@ export type SingleTabProps<T extends string = string> = {
|
||||
hide?: boolean;
|
||||
disabled?: boolean;
|
||||
pill?: string | React.ReactElement;
|
||||
cards?: LayoutCard[];
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user