diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx index 5a024b1767..3a11927de6 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx @@ -1,6 +1,15 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; -import { useAddDraftViewForFieldRelationTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForFieldRelationTableWidget'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { isFieldMetadataItemAvailableAsCalendarField } from '@/object-record/record-calendar/utils/isFieldMetadataItemAvailableAsCalendarField'; import { getFieldWidgetAvailableDisplayModes } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; +import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect'; +import { useAddDraftViewForFieldRelationTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForFieldRelationTableWidget'; +import { + type RecordTableWidgetLayoutViewType, + useRecordTableWidgetLayoutCallbacks, +} from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetLayoutCallbacks'; +import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay'; +import { isFieldMetadataItemAvailableAsWidgetGroupByField } from '@/page-layout/widgets/record-table/utils/isFieldMetadataItemAvailableAsWidgetGroupByField'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; @@ -13,11 +22,12 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useLingui } from '@lingui/react/macro'; -import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { type IconComponent, + IconCalendar, IconFileText, + IconId, IconLayoutKanban, IconListDetails, IconTable, @@ -25,17 +35,22 @@ import { import { MenuItemSelect } from 'twenty-ui/navigation'; import { FieldDisplayMode, + ViewType, type FieldConfiguration, } from '~/generated-metadata/graphql'; const DISPLAY_MODE_ICONS: Record = { [FieldDisplayMode.FIELD]: IconListDetails, - [FieldDisplayMode.CARD]: IconLayoutKanban, + [FieldDisplayMode.CARD]: IconId, [FieldDisplayMode.EDITOR]: IconFileText, [FieldDisplayMode.VIEW]: IconListDetails, [FieldDisplayMode.TABLE]: IconTable, }; +// One flat picker: inline display modes (Field / Card / Editor) followed by the +// embedded-view layouts (Table / Kanban / Calendar). Picking a layout selects +// the TABLE display mode under the hood — users choose "Kanban" directly +// instead of "Table" first and a layout second. export const FieldWidgetLayoutDropdownContent = () => { const { t } = useLingui(); @@ -49,21 +64,64 @@ export const FieldWidgetLayoutDropdownContent = () => { const currentDisplayMode = fieldConfiguration?.fieldDisplayMode; const currentFieldMetadataId = fieldConfiguration?.fieldMetadataId; + const currentViewId = fieldConfiguration?.viewId ?? null; const { fieldMetadataItem } = useFieldMetadataItemById( currentFieldMetadataId ?? '', ); - const layoutOptions = useMemo( - () => - fieldMetadataItem - ? getFieldWidgetAvailableDisplayModes( - fieldMetadataItem.type, - fieldMetadataItem.relation?.type, - ) - : [FieldDisplayMode.FIELD], - [fieldMetadataItem], + const availableDisplayModes = fieldMetadataItem + ? getFieldWidgetAvailableDisplayModes( + fieldMetadataItem.type, + fieldMetadataItem.relation?.type, + ) + : [FieldDisplayMode.FIELD]; + + const inlineDisplayModes = availableDisplayModes.filter( + (displayMode) => displayMode !== FieldDisplayMode.TABLE, ); + const hasEmbeddedViewLayouts = availableDisplayModes.includes( + FieldDisplayMode.TABLE, + ); + + const targetObjectMetadataId = + fieldMetadataItem?.relation?.targetObjectMetadata.id; + const inverseFieldMetadataId = + fieldMetadataItem?.relation?.targetFieldMetadata.id; + + const { objectMetadataItems } = useObjectMetadataItems(); + const targetObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.id === targetObjectMetadataId, + ); + + const defaultGroupByFieldMetadataItem = + (targetObjectMetadataItem?.readableFields ?? []).find( + isFieldMetadataItemAvailableAsWidgetGroupByField, + ) ?? null; + + const defaultCalendarFieldMetadataItem = + (targetObjectMetadataItem?.readableFields ?? []).find( + isFieldMetadataItemAvailableAsCalendarField, + ) ?? null; + + const isKanbanAvailable = isDefined(defaultGroupByFieldMetadataItem); + const isCalendarAvailable = isDefined(defaultCalendarFieldMetadataItem); + + const { view: embeddedWidgetView } = useRecordTableWidgetViewForDisplay({ + viewId: currentViewId ?? '', + widgetId: widgetInEditMode?.id ?? '', + pageLayoutId, + }); + + const isTableDisplayMode = currentDisplayMode === FieldDisplayMode.TABLE; + + const currentEmbeddedViewType: RecordTableWidgetLayoutViewType = + embeddedWidgetView?.type === ViewType.KANBAN_WIDGET + ? ViewType.KANBAN_WIDGET + : embeddedWidgetView?.type === ViewType.CALENDAR_WIDGET + ? ViewType.CALENDAR_WIDGET + : ViewType.TABLE_WIDGET; const dropdownId = useAvailableComponentInstanceIdOrThrow( DropdownComponentInstanceContext, @@ -80,18 +138,37 @@ export const FieldWidgetLayoutDropdownContent = () => { const { addDraftViewForFieldRelationTableWidget } = useAddDraftViewForFieldRelationTableWidget(pageLayoutId); + const { handleLayoutChange } = useRecordTableWidgetLayoutCallbacks({ + pageLayoutId, + widgetId: widgetInEditMode?.id ?? '', + }); + const { closeDropdown } = useCloseDropdown(); - const handleSelectLayout = (fieldDisplayMode: FieldDisplayMode) => { - const targetObjectMetadataId = - fieldMetadataItem?.relation?.targetObjectMetadata.id; - const inverseFieldMetadataId = - fieldMetadataItem?.relation?.targetFieldMetadata.id; + const handleSelectDisplayMode = (fieldDisplayMode: FieldDisplayMode) => { + updateCurrentWidgetConfig({ + configToUpdate: { + fieldDisplayMode, + }, + }); + closeDropdown(); + }; + + const handleSelectViewLayout = ( + targetViewType: RecordTableWidgetLayoutViewType, + ) => { + if (!isDefined(widgetInEditMode)) { + return; + } + if (targetViewType === ViewType.KANBAN_WIDGET && !isKanbanAvailable) { + return; + } + if (targetViewType === ViewType.CALENDAR_WIDGET && !isCalendarAvailable) { + return; + } if ( - fieldDisplayMode === FieldDisplayMode.TABLE && - !isDefined(fieldConfiguration?.viewId) && - isDefined(widgetInEditMode) && + !isDefined(currentViewId) && isDefined(targetObjectMetadataId) && isDefined(inverseFieldMetadataId) ) { @@ -103,55 +180,136 @@ export const FieldWidgetLayoutDropdownContent = () => { updateCurrentWidgetConfig({ configToUpdate: { - fieldDisplayMode, + fieldDisplayMode: FieldDisplayMode.TABLE, viewId, }, }); - closeDropdown(); - return; + } else { + updateCurrentWidgetConfig({ + configToUpdate: { + fieldDisplayMode: FieldDisplayMode.TABLE, + }, + }); } - updateCurrentWidgetConfig({ - configToUpdate: { - fieldDisplayMode, - }, + handleLayoutChange({ + targetViewType, + defaultGroupByFieldMetadataItem, + defaultCalendarFieldMetadataItem, }); closeDropdown(); }; - const layoutLabels: Record = { + const displayModeLabels: Record = { [FieldDisplayMode.FIELD]: t`Field`, [FieldDisplayMode.CARD]: t`Card`, [FieldDisplayMode.EDITOR]: t`Editor`, - [FieldDisplayMode.TABLE]: t`Table`, }; return ( + {/* The widget's draft snapshot is normally seeded by the table-family + renderer; while displayed as Field/Card that renderer isn't mounted, + so seed the draft here (idempotent) for direct e.g. Card -> Kanban + switches. */} + {isDefined(currentViewId) && isDefined(widgetInEditMode) && ( + + )} - {layoutOptions.map((displayMode) => ( + {inlineDisplayModes.map((displayMode) => ( { - handleSelectLayout(displayMode); + handleSelectDisplayMode(displayMode); }} > { - handleSelectLayout(displayMode); + handleSelectDisplayMode(displayMode); }} /> ))} + {hasEmbeddedViewLayouts && ( + <> + handleSelectViewLayout(ViewType.TABLE_WIDGET)} + > + handleSelectViewLayout(ViewType.TABLE_WIDGET)} + /> + + handleSelectViewLayout(ViewType.KANBAN_WIDGET)} + > + handleSelectViewLayout(ViewType.KANBAN_WIDGET)} + /> + + handleSelectViewLayout(ViewType.CALENDAR_WIDGET)} + > + handleSelectViewLayout(ViewType.CALENDAR_WIDGET)} + /> + + + )} ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx index 5a86d1f1e5..f7fb7380eb 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx @@ -3,30 +3,39 @@ import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuIt import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; import { useRecordTableWidgetViewFieldItems } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewFieldItems'; +import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory'; import { FieldWidgetFieldDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent'; import { FieldWidgetLayoutDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent'; +import { WidgetViewLayoutSettingsRows } from '@/side-panel/pages/page-layout/components/record-table-settings/WidgetViewLayoutSettingsRows'; import { WidgetSettingsManageSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsManageSection'; import { WidgetSettingsPlacementSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsPlacementSection'; import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; import { useWidgetSettingsPlacementSelectableItemIds } from '@/side-panel/pages/page-layout/hooks/useWidgetSettingsPlacementSelectableItemIds'; +import { getWidgetViewLayoutSettingsItemIds } from '@/side-panel/pages/page-layout/utils/getWidgetViewLayoutSettingsItemIds'; import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { + IconCalendar, + IconLayoutKanban, IconLayoutSidebarRight, IconList, IconListDetails, + IconTable, } from 'twenty-ui/icon'; import { + FeatureFlagKey, FieldDisplayMode, + ViewType, type FieldConfiguration, } from '~/generated-metadata/graphql'; @@ -74,12 +83,41 @@ export const SidePanelRecordPageFieldSettings = () => { pageLayoutId, }); + // A relation field widget in table display mode embeds a widget view scoped to + // the current record's related records; its source object is the relation + // target, not the record page's own object. + const targetObjectMetadataId = + currentFieldMetadataItem?.relation?.targetObjectMetadata.id; + + const { view: embeddedWidgetView } = useRecordTableWidgetViewForDisplay({ + viewId: currentViewId ?? '', + widgetId: widgetInEditMode?.id ?? '', + pageLayoutId, + }); + + const isCalendarWeekViewEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED, + ); + if (!isDefined(widgetInEditMode)) { return null; } const isTableDisplayMode = currentDisplayMode === FieldDisplayMode.TABLE; + const showViewLayoutRows = + isTableDisplayMode && + isDefined(targetObjectMetadataId) && + isDefined(currentViewId); + + const isEmbeddedViewKanbanLayout = + embeddedWidgetView?.type === ViewType.KANBAN_WIDGET; + const isEmbeddedViewCalendarLayout = + embeddedWidgetView?.type === ViewType.CALENDAR_WIDGET; + const embeddedViewHasGroupBy = isDefined( + embeddedWidgetView?.mainGroupByFieldMetadataId, + ); + const visibleFieldsCount = recordTableWidgetViewFieldItems.filter( (item) => item.viewField.isVisible, ).length; @@ -99,13 +137,35 @@ export const SidePanelRecordPageFieldSettings = () => { [FieldDisplayMode.TABLE]: t`Table`, }; - const layoutLabel = isDefined(currentDisplayMode) - ? (displayModeLabels[currentDisplayMode] ?? '') - : ''; + const layoutLabel = isTableDisplayMode + ? isEmbeddedViewKanbanLayout + ? t`Kanban` + : isEmbeddedViewCalendarLayout + ? t`Calendar` + : t`Table` + : isDefined(currentDisplayMode) + ? (displayModeLabels[currentDisplayMode] ?? '') + : ''; + + const layoutRowIcon = isTableDisplayMode + ? isEmbeddedViewKanbanLayout + ? IconLayoutKanban + : isEmbeddedViewCalendarLayout + ? IconCalendar + : IconTable + : IconLayoutSidebarRight; const selectableItemIds = [ 'field', 'layout', + ...(showViewLayoutRows + ? getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: isEmbeddedViewCalendarLayout, + isCalendarWeekViewEnabled, + hasGroupBy: embeddedViewHasGroupBy, + isLayoutRowHidden: true, + }) + : []), ...(isTableDisplayMode ? ['fields'] : []), WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION, WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT, @@ -139,7 +199,7 @@ export const SidePanelRecordPageFieldSettings = () => { @@ -151,6 +211,17 @@ export const SidePanelRecordPageFieldSettings = () => { contextualTextPosition="right" /> + {isTableDisplayMode && + isDefined(targetObjectMetadataId) && + isDefined(currentViewId) && ( + + )} {isTableDisplayMode && ( { + const isCalendarWeekViewEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED, + ); + + const { handleShouldHideEmptyGroupsChange } = + useRecordTableWidgetLayoutCallbacks({ + pageLayoutId, + widgetId, + }); + + const { view: widgetView } = useRecordTableWidgetViewForDisplay({ + viewId, + widgetId, + pageLayoutId, + }); + + const mainGroupByFieldMetadataId = + widgetView?.mainGroupByFieldMetadataId ?? null; + const shouldHideEmptyGroups = widgetView?.shouldHideEmptyGroups ?? false; + + const isKanbanLayout = widgetView?.type === ViewType.KANBAN_WIDGET; + const isCalendarLayout = widgetView?.type === ViewType.CALENDAR_WIDGET; + const currentLayoutViewType = isKanbanLayout + ? ViewType.KANBAN_WIDGET + : isCalendarLayout + ? ViewType.CALENDAR_WIDGET + : ViewType.TABLE_WIDGET; + + const calendarFieldMetadataId = widgetView?.calendarFieldMetadataId ?? null; + + const currentCalendarLayout = + widgetView?.calendarLayout ?? ViewCalendarLayout.MONTH; + + const calendarLayoutLabel = + currentCalendarLayout === ViewCalendarLayout.DAY + ? t`Day` + : currentCalendarLayout === ViewCalendarLayout.WEEK + ? t`Week` + : t`Month`; + + const { objectMetadataItems } = useObjectMetadataItems(); + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.id === objectMetadataId, + ); + + const mainGroupByFieldLabel = isDefined(mainGroupByFieldMetadataId) + ? (objectMetadataItem?.fields.find( + (fieldMetadataItem) => + fieldMetadataItem.id === mainGroupByFieldMetadataId, + )?.label ?? t`None`) + : t`None`; + + const calendarFieldLabel = isDefined(calendarFieldMetadataId) + ? (objectMetadataItem?.fields.find( + (fieldMetadataItem) => fieldMetadataItem.id === calendarFieldMetadataId, + )?.label ?? t`None`) + : t`None`; + + const hasGroupBy = isDefined(mainGroupByFieldMetadataId); + + return ( + <> + {!isLayoutRowHidden && ( + + + + + } + dropdownPlacement="bottom-end" + hasSubMenu + description={ + isKanbanLayout + ? t`Kanban` + : isCalendarLayout + ? t`Calendar` + : t`Table` + } + contextualTextPosition="right" + /> + + )} + {isCalendarLayout && ( + + + + + } + dropdownPlacement="bottom-end" + hasSubMenu + description={calendarFieldLabel} + contextualTextPosition="right" + /> + + )} + {isCalendarLayout && isCalendarWeekViewEnabled && ( + + + + + } + dropdownPlacement="bottom-end" + hasSubMenu + description={calendarLayoutLabel} + contextualTextPosition="right" + /> + + )} + {!isCalendarLayout && ( + + + + + } + dropdownPlacement="bottom-end" + hasSubMenu + description={mainGroupByFieldLabel} + contextualTextPosition="right" + /> + + )} + {!isCalendarLayout && hasGroupBy && ( + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/getWidgetViewLayoutSettingsItemIds.test.ts b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/getWidgetViewLayoutSettingsItemIds.test.ts new file mode 100644 index 0000000000..acc6c7b33e --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/getWidgetViewLayoutSettingsItemIds.test.ts @@ -0,0 +1,62 @@ +import { getWidgetViewLayoutSettingsItemIds } from '@/side-panel/pages/page-layout/utils/getWidgetViewLayoutSettingsItemIds'; + +describe('getWidgetViewLayoutSettingsItemIds', () => { + it('returns the layout and group-by rows for a table layout', () => { + expect( + getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: false, + isCalendarWeekViewEnabled: false, + hasGroupBy: false, + }), + ).toEqual(['object-view-layout', 'record-table-group-by']); + }); + + it('appends the hide-empty-groups row while grouped', () => { + expect( + getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: false, + isCalendarWeekViewEnabled: false, + hasGroupBy: true, + }), + ).toEqual([ + 'object-view-layout', + 'record-table-group-by', + 'record-table-hide-empty-groups', + ]); + }); + + it('swaps group-by rows for the calendar field row on a calendar layout', () => { + expect( + getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: true, + isCalendarWeekViewEnabled: false, + hasGroupBy: true, + }), + ).toEqual(['object-view-layout', 'record-table-calendar-field']); + }); + + it('includes the calendar view row when the week/day flag is enabled', () => { + expect( + getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: true, + isCalendarWeekViewEnabled: true, + hasGroupBy: false, + }), + ).toEqual([ + 'object-view-layout', + 'record-table-calendar-field', + 'record-table-calendar-layout', + ]); + }); + + it('omits the layout row when it is hidden', () => { + expect( + getWidgetViewLayoutSettingsItemIds({ + isCalendarLayout: false, + isCalendarWeekViewEnabled: false, + hasGroupBy: false, + isLayoutRowHidden: true, + }), + ).toEqual(['record-table-group-by']); + }); +}); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/getWidgetViewLayoutSettingsItemIds.ts b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/getWidgetViewLayoutSettingsItemIds.ts new file mode 100644 index 0000000000..eb5e2182a7 --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/getWidgetViewLayoutSettingsItemIds.ts @@ -0,0 +1,22 @@ +export const getWidgetViewLayoutSettingsItemIds = ({ + isCalendarLayout, + isCalendarWeekViewEnabled, + hasGroupBy, + isLayoutRowHidden = false, +}: { + isCalendarLayout: boolean; + isCalendarWeekViewEnabled: boolean; + hasGroupBy: boolean; + isLayoutRowHidden?: boolean; +}) => [ + ...(isLayoutRowHidden ? [] : ['object-view-layout']), + ...(isCalendarLayout + ? [ + 'record-table-calendar-field', + ...(isCalendarWeekViewEnabled ? ['record-table-calendar-layout'] : []), + ] + : ['record-table-group-by']), + ...(!isCalendarLayout && hasGroupBy + ? ['record-table-hide-empty-groups'] + : []), +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-widget-upsert.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-widget-upsert.service.ts index c8fa0672e2..6f2c5f2eca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-widget-upsert.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-widget-upsert.service.ts @@ -192,13 +192,6 @@ export class ViewWidgetUpsertService { }; if (isDefined(input.view)) { - if (!isRecordTableWidget) { - throw new ViewException( - t`View settings can only be updated on record table widgets`, - ViewExceptionCode.INVALID_VIEW_DATA, - ); - } - if ( isDefined(input.view.type) && !ALLOWED_WIDGET_VIEW_TYPES.includes(input.view.type) diff --git a/packages/twenty-server/test/integration/metadata/suites/view/upsert-view-widget-view-settings.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view/upsert-view-widget-view-settings.integration-spec.ts index 2094cac2c7..ed86b80c56 100644 --- a/packages/twenty-server/test/integration/metadata/suites/view/upsert-view-widget-view-settings.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/view/upsert-view-widget-view-settings.integration-spec.ts @@ -20,6 +20,7 @@ import { ViewType, } from 'twenty-shared/types'; +import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; @@ -404,4 +405,110 @@ describe('upsertViewWidget view settings', () => { ), ).toBe(true); }); + + describe('field widget in table display mode', () => { + let fieldWidgetId: string; + let fieldWidgetViewId: string; + + beforeAll(async () => { + const { data: fieldViewData } = await createOneView({ + expectToFail: false, + input: { + name: 'testFieldWidgetEmbeddedView', + objectMetadataId, + icon: 'IconTable', + type: ViewType.TABLE_WIDGET, + }, + }); + + fieldWidgetViewId = fieldViewData.createView.id; + + const { data: fieldWidgetData } = await createOnePageLayoutWidget({ + expectToFail: false, + input: { + title: 'Test Field Table Widget For View Settings', + type: WidgetType.FIELD, + pageLayoutTabId, + objectMetadataId, + gridPosition: { row: 1, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: { + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId: selectFieldMetadataId, + fieldDisplayMode: FieldDisplayMode.TABLE, + viewId: fieldWidgetViewId, + }, + }, + }); + + fieldWidgetId = fieldWidgetData.createPageLayoutWidget.id; + }); + + afterAll(async () => { + await destroyOnePageLayoutWidget({ + expectToFail: false, + input: { id: fieldWidgetId }, + }); + await destroyOneView({ + expectToFail: false, + viewId: fieldWidgetViewId, + }); + }); + + it('should switch a field table widget embedded view to KANBAN_WIDGET', async () => { + const { data } = await upsertViewWidget({ + expectToFail: false, + input: { + widgetId: fieldWidgetId, + view: { + type: ViewType.KANBAN_WIDGET, + mainGroupByFieldMetadataId: selectFieldMetadataId, + }, + }, + gqlFields: VIEW_SETTINGS_GQL_FIELDS, + }); + + expect(data.upsertViewWidget.type).toBe(ViewType.KANBAN_WIDGET); + expect(data.upsertViewWidget.mainGroupByFieldMetadataId).toBe( + selectFieldMetadataId, + ); + }); + + it('should switch a field table widget embedded view to CALENDAR_WIDGET', async () => { + const { data } = await upsertViewWidget({ + expectToFail: false, + input: { + widgetId: fieldWidgetId, + view: { + type: ViewType.CALENDAR_WIDGET, + calendarLayout: ViewCalendarLayout.MONTH, + calendarFieldMetadataId: dateFieldMetadataId, + mainGroupByFieldMetadataId: null, + }, + }, + gqlFields: VIEW_SETTINGS_GQL_FIELDS, + }); + + expect(data.upsertViewWidget.type).toBe(ViewType.CALENDAR_WIDGET); + expect(data.upsertViewWidget.calendarFieldMetadataId).toBe( + dateFieldMetadataId, + ); + }); + + it('should still reject a field table widget switching to KANBAN_WIDGET without a main group by field', async () => { + const { errors } = await upsertViewWidget({ + expectToFail: true, + input: { + widgetId: fieldWidgetId, + view: { + type: ViewType.KANBAN_WIDGET, + mainGroupByFieldMetadataId: null, + }, + }, + }); + + expect(JSON.stringify(errors)).toContain( + 'Kanban view must have a main group by field', + ); + }); + }); });