Migrate field widgets to backend (#18808)

- Renamed FieldConfiguration's layout field to fieldDisplayMode as it
caused issues with the layout field of BarChartConfiguration
- Create relation Field widgets for standard objects

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-03-23 01:26:00 +01:00
committed by GitHub
parent d90d2e3151
commit be89ef30cd
38 changed files with 1388 additions and 790 deletions
@@ -1,9 +1,9 @@
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -16,11 +16,12 @@ import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLa
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useLingui } from '@lingui/react/macro';
import { useStore } from 'jotai';
import { useCallback, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { logError } from '~/utils/logError';
@@ -36,6 +37,10 @@ export const useSaveLayoutCustomization = () => {
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const save = useCallback(async () => {
setIsSaving(true);
try {
@@ -92,7 +97,10 @@ export const useSaveLayoutCustomization = () => {
);
if (isPageLayoutStructureDirty) {
const updateInput = convertPageLayoutDraftToUpdateInput(draft);
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets:
!isRecordPageLayoutEditingEnabled,
});
const result = await updatePageLayoutWithTabsAndWidgets(
pageLayoutId,
updateInput,
@@ -107,6 +115,7 @@ export const useSaveLayoutCustomization = () => {
transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
!isRecordPageLayoutEditingEnabled &&
persistedLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
@@ -10,10 +10,11 @@ import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLa
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type PageLayoutInitializationQueryEffectProps = {
@@ -26,9 +27,17 @@ export const PageLayoutInitializationQueryEffect = ({
const [pageLayoutIsInitialized, setPageLayoutIsInitialized] =
useAtomComponentState(pageLayoutIsInitializedComponentState);
const basePageLayout = useBasePageLayout(pageLayoutId);
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
const basePageLayout = useBasePageLayout(pageLayoutId);
const pageLayoutWithRelationWidgets =
usePageLayoutWithRelationWidgets(basePageLayout);
const pageLayout = isRecordPageLayoutEditingEnabled
? basePageLayout
: pageLayoutWithRelationWidgets;
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
@@ -7,8 +7,10 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type PageLayoutRendererProps = {
pageLayoutId: string;
@@ -19,6 +21,10 @@ export const PageLayoutRenderer = ({
}: PageLayoutRendererProps) => {
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType,
@@ -42,7 +48,9 @@ export const PageLayoutRenderer = ({
>
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
{!isRecordPageLayoutEditingEnabled && (
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
)}
<PageLayoutRendererContent />
</PageLayoutEditModeProvider>
</TabListComponentInstanceContext.Provider>
@@ -161,6 +161,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
}
... on FieldConfiguration {
configurationType
fieldDisplayMode
fieldMetadataId
}
... on FieldRichTextConfiguration {
configurationType
@@ -10,10 +10,11 @@ import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reI
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -40,11 +41,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const store = useStore();
const savePageLayout = useCallback(async () => {
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
shouldFilterDynamicRelationWidgets: !isRecordPageLayoutEditingEnabled,
});
const result = await updatePageLayoutWithTabsAndWidgets(
pageLayoutId,
@@ -60,6 +67,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
!isRecordPageLayoutEditingEnabled &&
persistedLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
@@ -77,6 +85,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
return result;
}, [
isRecordPageLayoutEditingEnabled,
pageLayoutCurrentLayoutsCallbackState,
pageLayoutDraftCallbackState,
pageLayoutId,
@@ -1,8 +1,11 @@
import { type WidgetConfigurationType } from '~/generated-metadata/graphql';
import {
type FieldDisplayMode,
type WidgetConfigurationType,
} from '~/generated-metadata/graphql';
export type FieldConfiguration = {
__typename: 'FieldConfiguration';
configurationType: WidgetConfigurationType.FIELD;
fieldMetadataId: string;
layout: 'FIELD' | 'CARD' | 'VIEW';
fieldDisplayMode: FieldDisplayMode;
};
@@ -60,19 +60,19 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
expect(result.tabs[0].widgets[0].id).toBe('w1');
});
it('should filter out dynamic relation widgets', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
it('should handle multiple widget types', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const draft = makeDraft([makeTab('tab-1', [regularWidget, dynamicWidget])]);
const draft = makeDraft([makeTab('tab-1', [fieldsWidget, timelineWidget])]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(1);
expect(result.tabs[0].widgets[0].id).toBe('w1');
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
});
it('should map gridPosition correctly', () => {
@@ -121,24 +121,25 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
expect(result.tabs[1].id).toBe('tab-2');
});
it('should keep all non-dynamic widgets when multiple widget types exist', () => {
it('should keep all widgets when multiple widget types exist', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
const fieldWidget = makeWidget({
id: 'w3',
type: WidgetType.FIELD,
});
const draft = makeDraft([
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
makeTab('tab-1', [fieldsWidget, fieldWidget, timelineWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
expect(result.tabs[0].widgets).toHaveLength(3);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w3', 'w2']);
});
it('should produce VERTICAL_LIST position with index from widget.position when tab is VERTICAL_LIST', () => {
@@ -326,4 +327,81 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
columnSpan: 6,
});
});
describe('shouldFilterDynamicRelationWidgets', () => {
it('should filter out dynamic relation widgets when shouldFilterDynamicRelationWidgets is true', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: true,
});
expect(result.tabs[0].widgets).toHaveLength(1);
expect(result.tabs[0].widgets[0].id).toBe('w1');
});
it('should keep all non-dynamic widgets when shouldFilterDynamicRelationWidgets is true and multiple widget types exist', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
});
const draft = makeDraft([
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: true,
});
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
});
it('should not filter dynamic relation widgets when shouldFilterDynamicRelationWidgets is false', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: false,
});
expect(result.tabs[0].widgets).toHaveLength(2);
});
it('should not filter dynamic relation widgets by default', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(2);
});
});
});
@@ -4,6 +4,7 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
import {
FieldDisplayMode,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
@@ -195,7 +196,7 @@ describe('injectRelationWidgetsIntoLayout', () => {
expect(injectedWidget.configuration).toEqual(
expect.objectContaining({
fieldMetadataId: 'f1',
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
}),
);
});
@@ -40,19 +40,25 @@ const buildWidgetPosition = (
export const convertPageLayoutDraftToUpdateInput = (
pageLayoutDraft: DraftPageLayout,
options?: { shouldFilterDynamicRelationWidgets?: boolean },
): UpdatePageLayoutWithTabsInput => {
const shouldFilter = options?.shouldFilterDynamicRelationWidgets ?? false;
return {
name: pageLayoutDraft.name,
type: pageLayoutDraft.type,
objectMetadataId: pageLayoutDraft.objectMetadataId ?? null,
tabs: pageLayoutDraft.tabs.map((tab) => ({
id: tab.id,
title: tab.title,
position: tab.position,
layoutMode: tab.layoutMode,
widgets: tab.widgets
.filter((widget) => !isDynamicRelationWidget(widget))
.map((widget, widgetIndex) => ({
tabs: pageLayoutDraft.tabs.map((tab) => {
const widgets = shouldFilter
? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget))
: tab.widgets;
return {
id: tab.id,
title: tab.title,
position: tab.position,
layoutMode: tab.layoutMode,
widgets: widgets.map((widget, widgetIndex) => ({
id: widget.id,
pageLayoutTabId: widget.pageLayoutTabId,
title: widget.title,
@@ -71,6 +77,7 @@ export const convertPageLayoutDraftToUpdateInput = (
),
configuration: widget.configuration ?? null,
})),
})),
};
}),
};
};
@@ -1,5 +1,6 @@
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import {
FieldDisplayMode,
PageLayoutTabLayoutMode,
WidgetConfigurationType,
WidgetType,
@@ -28,7 +29,7 @@ export const createDefaultFieldWidget = ({
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
gridPosition: {
__typename: 'GridPosition',
@@ -4,6 +4,7 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
import { isDefined } from 'twenty-shared/utils';
import {
FieldDisplayMode,
PageLayoutTabLayoutMode,
WidgetConfigurationType,
WidgetType,
@@ -38,7 +39,7 @@ const getRelationFieldWidgetToInsert = (
__typename: 'FieldConfiguration' as const,
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
layout: 'CARD' as const,
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
@@ -7,14 +7,14 @@ import { expect, within } from 'storybook/test';
import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
@@ -35,14 +35,15 @@ import { widgetCardHoveredComponentFamilyState } from '@/page-layout/widgets/sta
import { type WidgetCardVariant } from '@/page-layout/widgets/types/WidgetCardVariant';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import {
GraphOrderBy,
WidgetType,
AggregateOperations,
AxisNameDisplay,
BarChartLayout,
FieldDisplayMode,
GraphOrderBy,
PageLayoutTabLayoutMode,
PageLayoutType,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
@@ -1023,7 +1024,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: accountOwnerField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1142,7 +1143,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyPeopleField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1253,7 +1254,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyPeopleField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -15,9 +15,9 @@ import { assertFieldWidgetOrThrow } from '@/page-layout/widgets/field/utils/asse
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { SidePanelProvider } from '@/ui/layout/side-panel/contexts/SidePanelContext';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { isDefined } from 'twenty-shared/utils';
import {
AnimatedPlaceholder,
@@ -27,6 +27,7 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { FieldDisplayMode } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -92,10 +93,10 @@ export const FieldWidget = ({ widget }: FieldWidgetProps) => {
labelWidth: 90,
});
const layout = widget.configuration.layout;
const fieldDisplayMode = widget.configuration.fieldDisplayMode;
if (isFieldMorphRelation(fieldDefinition)) {
if (layout === 'CARD') {
if (fieldDisplayMode === FieldDisplayMode.CARD) {
return (
<FieldWidgetMorphRelationCard
fieldDefinition={fieldDefinition}
@@ -115,7 +116,7 @@ export const FieldWidget = ({ widget }: FieldWidgetProps) => {
}
if (isFieldRelation(fieldDefinition)) {
if (layout === 'CARD') {
if (fieldDisplayMode === FieldDisplayMode.CARD) {
return (
<FieldWidgetRelationCard
fieldDefinition={fieldDefinition}
@@ -4,10 +4,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
import { MemoryRouter } from 'react-router-dom';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
@@ -23,8 +21,10 @@ import { FieldWidget } from '@/page-layout/widgets/field/components/FieldWidget'
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import {
FieldDisplayMode,
PageLayoutTabLayoutMode,
PageLayoutType,
WidgetConfigurationType,
@@ -36,6 +36,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow(
CoreObjectNameSingular.Company,
@@ -348,7 +349,7 @@ export const TextFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: nameField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -442,7 +443,7 @@ export const AddressFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: addressField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -539,7 +540,7 @@ export const NumberFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: employeesField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -633,7 +634,7 @@ export const LinkFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: linkedinField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -727,7 +728,7 @@ export const ManyToOneRelationFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: accountOwnerField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -831,7 +832,7 @@ export const OneToManyRelationFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyPeopleField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -927,7 +928,7 @@ export const BooleanFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: idealCustomerProfileField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1020,7 +1021,7 @@ export const CurrencyFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: annualRecurringRevenueField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1113,7 +1114,7 @@ export const EmailsFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: personEmailsField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1207,7 +1208,7 @@ export const PhonesFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: personPhonesField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1301,7 +1302,7 @@ export const SelectFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: opportunityStageField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1396,7 +1397,7 @@ export const MultiSelectFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyWorkPolicyField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1494,7 +1495,7 @@ export const TimelineActivityRelationFieldWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: timelineActivityWorkspaceMemberField.id,
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1593,7 +1594,7 @@ export const ManyToOneRelationCardWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: accountOwnerField.id,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1705,7 +1706,7 @@ export const OneToManyRelationCardWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyPeopleField.id,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1800,7 +1801,7 @@ export const TimelineActivityRelationCardWidget: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: timelineActivityWorkspaceMemberField.id,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1961,7 +1962,7 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: companyPeopleField.id,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
@@ -1,6 +1,7 @@
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { assertFieldWidgetOrThrow } from '@/page-layout/widgets/field/utils/assertFieldWidgetOrThrow';
import {
FieldDisplayMode,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
@@ -14,7 +15,7 @@ describe('assertFieldWidgetOrThrow', () => {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'fm-1',
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
} as PageLayoutWidget;
@@ -1,6 +1,7 @@
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
import {
FieldDisplayMode,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
@@ -14,7 +15,7 @@ describe('isFieldWidget', () => {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'fm-1',
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
} as PageLayoutWidget;
@@ -29,7 +30,7 @@ describe('isFieldWidget', () => {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'fm-1',
layout: 'FIELD',
fieldDisplayMode: FieldDisplayMode.FIELD,
},
} as PageLayoutWidget;