import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util'; import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util'; import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { findViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-view-groups.util'; import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util'; import { destroyOneView } from 'test/integration/metadata/suites/view/utils/destroy-one-view.util'; import { upsertViewWidget } from 'test/integration/metadata/suites/view/utils/upsert-view-widget.util'; import { FieldMetadataType, PageLayoutTabLayoutMode, ViewCalendarLayout, ViewType, WidgetType, } 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'; const VIEW_SETTINGS_GQL_FIELDS = ` id type mainGroupByFieldMetadataId shouldHideEmptyGroups kanbanAggregateOperation kanbanAggregateOperationFieldMetadataId calendarLayout calendarFieldMetadataId calendarEndFieldMetadataId `; describe('upsertViewWidget view settings', () => { let objectMetadataId: string; let selectFieldMetadataId: string; let dateFieldMetadataId: string; let pageLayoutId: string; let pageLayoutTabId: string; let widgetId: string; let viewId: string; beforeAll(async () => { const { data: { createOneObject: { id: createdObjectMetadataId }, }, } = await createOneObjectMetadata({ expectToFail: false, input: { nameSingular: 'widgetViewSettingsTestObject', namePlural: 'widgetViewSettingsTestObjects', labelSingular: 'Widget View Settings Test Object', labelPlural: 'Widget View Settings Test Objects', icon: 'IconTestTube', }, }); objectMetadataId = createdObjectMetadataId; const { data: { createOneField: { id: createdSelectFieldMetadataId }, }, } = await createOneFieldMetadata({ expectToFail: false, input: { name: 'stage', type: FieldMetadataType.SELECT, label: 'Stage', objectMetadataId, options: [ { label: 'New', value: 'NEW', color: 'blue', position: 0 }, { label: 'Ongoing', value: 'ONGOING', color: 'red', position: 1 }, { label: 'Done', value: 'DONE', color: 'green', position: 2 }, ], }, gqlFields: 'id', }); selectFieldMetadataId = createdSelectFieldMetadataId; const { data: { createOneField: { id: createdDateFieldMetadataId }, }, } = await createOneFieldMetadata({ expectToFail: false, input: { name: 'eventDate', type: FieldMetadataType.DATE, label: 'Event Date', objectMetadataId, }, gqlFields: 'id', }); dateFieldMetadataId = createdDateFieldMetadataId; const { data: layoutData } = await createOnePageLayout({ expectToFail: false, input: { name: 'Test Page Layout For View Settings' }, }); pageLayoutId = layoutData.createPageLayout.id; const { data: tabData } = await createOnePageLayoutTab({ expectToFail: false, input: { title: 'Test Tab For View Settings', pageLayoutId, }, }); pageLayoutTabId = tabData.createPageLayoutTab.id; const { data: viewData } = await createOneView({ expectToFail: false, input: { name: 'testWidgetViewSettingsView', objectMetadataId, icon: 'IconTable', type: ViewType.TABLE_WIDGET, }, }); viewId = viewData.createView.id; const { data: widgetData } = await createOnePageLayoutWidget({ expectToFail: false, input: { title: 'Test Record Table Widget For View Settings', type: WidgetType.RECORD_TABLE, pageLayoutTabId, objectMetadataId, position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 1, columnSpan: 1, }, configuration: { configurationType: WidgetConfigurationType.RECORD_TABLE, viewId, }, }, }); widgetId = widgetData.createPageLayoutWidget.id; }); afterAll(async () => { await destroyOnePageLayoutWidget({ expectToFail: false, input: { id: widgetId }, }); await destroyOneView({ expectToFail: false, viewId, }); await destroyOnePageLayoutTab({ expectToFail: false, input: { id: pageLayoutTabId }, }); await destroyOnePageLayout({ expectToFail: false, input: { id: pageLayoutId }, }); await updateOneObjectMetadata({ expectToFail: false, input: { idToUpdate: objectMetadataId, updatePayload: { isActive: false }, }, }); await deleteOneObjectMetadata({ expectToFail: false, input: { idToDelete: objectMetadataId }, }); }); it('should reject a non-widget view type', async () => { const { errors } = await upsertViewWidget({ expectToFail: true, input: { widgetId, view: { type: ViewType.KANBAN, mainGroupByFieldMetadataId: selectFieldMetadataId, }, }, }); expect(errors?.[0]?.message).toContain( 'Widget views must use a widget view type', ); }); it('should switch the widget view to LIST_WIDGET', async () => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, view: { type: ViewType.LIST_WIDGET, }, }, gqlFields: VIEW_SETTINGS_GQL_FIELDS, }); expect(data.upsertViewWidget.type).toBe(ViewType.LIST_WIDGET); }); it('should reject switching to KANBAN_WIDGET without a main group by field', async () => { const { errors } = await upsertViewWidget({ expectToFail: true, input: { widgetId, view: { type: ViewType.KANBAN_WIDGET, }, }, }); expect(JSON.stringify(errors)).toContain( 'Kanban view must have a main group by field', ); }); it('should switch the widget view to KANBAN_WIDGET and auto-create view groups', async () => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, 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, ); const { data: viewGroupsData } = await findViewGroups({ expectToFail: false, viewId, }); const fieldValues = viewGroupsData.getViewGroups.map( (viewGroup) => viewGroup.fieldValue, ); expect(fieldValues).toEqual( expect.arrayContaining(['NEW', 'ONGOING', 'DONE']), ); }); it('should reject switching to CALENDAR_WIDGET without a calendar field', async () => { const { errors } = await upsertViewWidget({ expectToFail: true, input: { widgetId, view: { type: ViewType.CALENDAR_WIDGET, mainGroupByFieldMetadataId: null, }, }, }); expect(JSON.stringify(errors)).toContain( 'Calendar view must have a calendar field', ); }); it.each([ViewCalendarLayout.DAY, ViewCalendarLayout.WEEK])( 'should allow the %s layout on a CALENDAR_WIDGET view', async (calendarLayout) => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, view: { type: ViewType.CALENDAR_WIDGET, calendarLayout, calendarFieldMetadataId: dateFieldMetadataId, mainGroupByFieldMetadataId: null, }, }, gqlFields: VIEW_SETTINGS_GQL_FIELDS, }); expect(data.upsertViewWidget.type).toBe(ViewType.CALENDAR_WIDGET); expect(data.upsertViewWidget.calendarLayout).toBe(calendarLayout); }, ); it('should switch the widget view to CALENDAR_WIDGET with a date field and layout', async () => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, 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.calendarLayout).toBe(ViewCalendarLayout.MONTH); expect(data.upsertViewWidget.calendarFieldMetadataId).toBe( dateFieldMetadataId, ); const { data: viewGroupsData } = await findViewGroups({ expectToFail: false, viewId, }); expect(viewGroupsData.getViewGroups).toHaveLength(0); }); it('should switch the widget view back to TABLE_WIDGET', async () => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, view: { type: ViewType.TABLE_WIDGET, }, }, gqlFields: VIEW_SETTINGS_GQL_FIELDS, }); expect(data.upsertViewWidget.type).toBe(ViewType.TABLE_WIDGET); }); it('should update view settings and view fields in a single call', async () => { const { data } = await upsertViewWidget({ expectToFail: false, input: { widgetId, view: { type: ViewType.KANBAN_WIDGET, mainGroupByFieldMetadataId: selectFieldMetadataId, shouldHideEmptyGroups: true, }, viewFields: [ { fieldMetadataId: selectFieldMetadataId, isVisible: true, position: 0, }, ], }, gqlFields: ` ${VIEW_SETTINGS_GQL_FIELDS} viewFields { id fieldMetadataId isVisible } `, }); expect(data.upsertViewWidget.type).toBe(ViewType.KANBAN_WIDGET); expect(data.upsertViewWidget.shouldHideEmptyGroups).toBe(true); expect( (data.upsertViewWidget.viewFields ?? []).some( (viewField) => viewField.fieldMetadataId === selectFieldMetadataId, ), ).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, position: { layoutMode: PageLayoutTabLayoutMode.GRID, 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', ); }); }); });