diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 1796f1e594..75bf1889ea 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -3947,6 +3947,7 @@ export type QueryGetCoreViewSortsArgs = { export type QueryGetCoreViewsArgs = { objectMetadataId?: InputMaybe; + viewTypes?: InputMaybe>; }; @@ -5750,9 +5751,9 @@ export type FindManyCommandMenuItemsQueryVariables = Exact<{ [key: string]: neve export type FindManyCommandMenuItemsQuery = { __typename?: 'Query', commandMenuItems: Array<{ __typename?: 'CommandMenuItem', id: string, workflowVersionId?: string | null, frontComponentId?: string | null, label: string, icon?: string | null, isPinned: boolean, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string } | null }> }; -export type PageLayoutFragmentFragment = { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null }; +export type PageLayoutFragmentFragment = { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null }; -export type PageLayoutTabFragmentFragment = { __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }; +export type PageLayoutTabFragmentFragment = { __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }; export type DuplicateDashboardMutationVariables = Exact<{ id: Scalars['UUID']; @@ -5766,7 +5767,7 @@ export type FindOnePageLayoutQueryVariables = Exact<{ }>; -export type FindOnePageLayoutQuery = { __typename?: 'Query', getPageLayout?: { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null } | null }; +export type FindOnePageLayoutQuery = { __typename?: 'Query', getPageLayout?: { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null } | null }; export type CreateFileMutationVariables = Exact<{ file: Scalars['Upload']; @@ -5975,7 +5976,7 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; -export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }; +export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }; export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; @@ -5983,12 +5984,12 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ }>; -export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: string, name: string, type: PageLayoutType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, position: number, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null } }; +export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: string, name: string, type: PageLayoutType, objectMetadataId?: string | null, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null } }; export type FindAllRecordPageLayoutsQueryVariables = Exact<{ [key: string]: never; }>; -export type FindAllRecordPageLayoutsQuery = { __typename?: 'Query', getPageLayouts: Array<{ __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null }> }; +export type FindAllRecordPageLayoutsQuery = { __typename?: 'Query', getPageLayouts: Array<{ __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null }> }; export type BarChartDataQueryVariables = Exact<{ input: BarChartDataInput; @@ -6473,11 +6474,13 @@ export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typen export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; +export type ViewFieldGroupFragmentFragment = { __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; + export type ViewFilterFragmentFragment = { __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }; export type ViewFilterGroupFragmentFragment = { __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }; -export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }; @@ -6488,7 +6491,7 @@ export type CreateCoreViewMutationVariables = Exact<{ }>; -export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type CreateCoreViewFieldMutationVariables = Exact<{ input: CreateViewFieldInput; @@ -6497,6 +6500,13 @@ export type CreateCoreViewFieldMutationVariables = Exact<{ export type CreateCoreViewFieldMutation = { __typename?: 'Mutation', createCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type CreateCoreViewFieldGroupMutationVariables = Exact<{ + input: CreateViewFieldGroupInput; +}>; + + +export type CreateCoreViewFieldGroupMutation = { __typename?: 'Mutation', createCoreViewFieldGroup: { __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; + export type CreateCoreViewFilterMutationVariables = Exact<{ input: CreateViewFilterInput; }>; @@ -6525,6 +6535,13 @@ export type CreateCoreViewSortMutationVariables = Exact<{ export type CreateCoreViewSortMutation = { __typename?: 'Mutation', createCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string } }; +export type CreateManyCoreViewFieldGroupsMutationVariables = Exact<{ + inputs: Array | CreateViewFieldGroupInput; +}>; + + +export type CreateManyCoreViewFieldGroupsMutation = { __typename?: 'Mutation', createManyCoreViewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; + export type CreateManyCoreViewFieldsMutationVariables = Exact<{ inputs: Array | CreateViewFieldInput; }>; @@ -6553,6 +6570,13 @@ export type DeleteCoreViewFieldMutationVariables = Exact<{ export type DeleteCoreViewFieldMutation = { __typename?: 'Mutation', deleteCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type DeleteCoreViewFieldGroupMutationVariables = Exact<{ + input: DeleteViewFieldGroupInput; +}>; + + +export type DeleteCoreViewFieldGroupMutation = { __typename?: 'Mutation', deleteCoreViewFieldGroup: { __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; + export type DeleteCoreViewFilterMutationVariables = Exact<{ input: DeleteViewFilterInput; }>; @@ -6629,7 +6653,7 @@ export type UpdateCoreViewMutationVariables = Exact<{ }>; -export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type UpdateCoreViewFieldMutationVariables = Exact<{ input: UpdateViewFieldInput; @@ -6638,6 +6662,13 @@ export type UpdateCoreViewFieldMutationVariables = Exact<{ export type UpdateCoreViewFieldMutation = { __typename?: 'Mutation', updateCoreViewField: { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type UpdateCoreViewFieldGroupMutationVariables = Exact<{ + input: UpdateViewFieldGroupInput; +}>; + + +export type UpdateCoreViewFieldGroupMutation = { __typename?: 'Mutation', updateCoreViewFieldGroup: { __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; + export type UpdateCoreViewFilterMutationVariables = Exact<{ input: UpdateViewFilterInput; }>; @@ -6668,10 +6699,19 @@ export type UpdateCoreViewSortMutationVariables = Exact<{ export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string } }; -export type FindAllCoreViewsQueryVariables = Exact<{ [key: string]: never; }>; +export type FindAllCoreViewsQueryVariables = Exact<{ + viewTypes?: InputMaybe | ViewType>; +}>; -export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; + +export type FindFieldsWidgetCoreViewsQueryVariables = Exact<{ + viewTypes?: InputMaybe | ViewType>; +}>; + + +export type FindFieldsWidgetCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindManyCoreViewFieldsQueryVariables = Exact<{ viewId: Scalars['String']; @@ -6710,17 +6750,18 @@ export type FindManyCoreViewSortsQuery = { __typename?: 'Query', getCoreViewSort export type FindManyCoreViewsQueryVariables = Exact<{ objectMetadataId?: InputMaybe; + viewTypes?: InputMaybe | ViewType>; }>; -export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindOneCoreViewQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; +export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; export type FindOneCoreViewFieldQueryVariables = Exact<{ id: Scalars['String']; @@ -7253,6 +7294,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` } ... on FieldsConfiguration { configurationType + viewId } ... on FilesConfiguration { configurationType @@ -7767,6 +7809,21 @@ export const ViewFieldFragmentFragmentDoc = gql` deletedAt } `; +export const ViewFieldGroupFragmentFragmentDoc = gql` + fragment ViewFieldGroupFragment on CoreViewFieldGroup { + id + name + position + isVisible + viewId + createdAt + updatedAt + deletedAt + viewFields { + ...ViewFieldFragment + } +} + ${ViewFieldFragmentFragmentDoc}`; export const ViewFilterFragmentFragmentDoc = gql` fragment ViewFilterFragment on CoreViewFilter { id @@ -7834,6 +7891,9 @@ export const ViewFragmentFragmentDoc = gql` viewFields { ...ViewFieldFragment } + viewFieldGroups { + ...ViewFieldGroupFragment + } viewFilters { ...ViewFilterFragment } @@ -7848,6 +7908,7 @@ export const ViewFragmentFragmentDoc = gql` } } ${ViewFieldFragmentFragmentDoc} +${ViewFieldGroupFragmentFragmentDoc} ${ViewFilterFragmentFragmentDoc} ${ViewFilterGroupFragmentFragmentDoc} ${ViewSortFragmentFragmentDoc} @@ -11557,24 +11618,16 @@ export const UpdatePageLayoutWithTabsAndWidgetsDocument = gql` name type objectMetadataId + defaultTabToFocusOnMobileAndSidePanelId createdAt updatedAt deletedAt tabs { - id - applicationId - title - position - pageLayoutId - widgets { - ...PageLayoutWidgetFragment - } - createdAt - updatedAt + ...PageLayoutTabFragment } } } - ${PageLayoutWidgetFragmentFragmentDoc}`; + ${PageLayoutTabFragmentFragmentDoc}`; export type UpdatePageLayoutWithTabsAndWidgetsMutationFn = Apollo.MutationFunction; /** @@ -14214,6 +14267,39 @@ export function useCreateCoreViewFieldMutation(baseOptions?: Apollo.MutationHook export type CreateCoreViewFieldMutationHookResult = ReturnType; export type CreateCoreViewFieldMutationResult = Apollo.MutationResult; export type CreateCoreViewFieldMutationOptions = Apollo.BaseMutationOptions; +export const CreateCoreViewFieldGroupDocument = gql` + mutation CreateCoreViewFieldGroup($input: CreateViewFieldGroupInput!) { + createCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } +} + ${ViewFieldGroupFragmentFragmentDoc}`; +export type CreateCoreViewFieldGroupMutationFn = Apollo.MutationFunction; + +/** + * __useCreateCoreViewFieldGroupMutation__ + * + * To run a mutation, you first call `useCreateCoreViewFieldGroupMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateCoreViewFieldGroupMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createCoreViewFieldGroupMutation, { data, loading, error }] = useCreateCoreViewFieldGroupMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateCoreViewFieldGroupMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateCoreViewFieldGroupDocument, options); + } +export type CreateCoreViewFieldGroupMutationHookResult = ReturnType; +export type CreateCoreViewFieldGroupMutationResult = Apollo.MutationResult; +export type CreateCoreViewFieldGroupMutationOptions = Apollo.BaseMutationOptions; export const CreateCoreViewFilterDocument = gql` mutation CreateCoreViewFilter($input: CreateViewFilterInput!) { createCoreViewFilter(input: $input) { @@ -14346,6 +14432,39 @@ export function useCreateCoreViewSortMutation(baseOptions?: Apollo.MutationHookO export type CreateCoreViewSortMutationHookResult = ReturnType; export type CreateCoreViewSortMutationResult = Apollo.MutationResult; export type CreateCoreViewSortMutationOptions = Apollo.BaseMutationOptions; +export const CreateManyCoreViewFieldGroupsDocument = gql` + mutation CreateManyCoreViewFieldGroups($inputs: [CreateViewFieldGroupInput!]!) { + createManyCoreViewFieldGroups(inputs: $inputs) { + ...ViewFieldGroupFragment + } +} + ${ViewFieldGroupFragmentFragmentDoc}`; +export type CreateManyCoreViewFieldGroupsMutationFn = Apollo.MutationFunction; + +/** + * __useCreateManyCoreViewFieldGroupsMutation__ + * + * To run a mutation, you first call `useCreateManyCoreViewFieldGroupsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateManyCoreViewFieldGroupsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createManyCoreViewFieldGroupsMutation, { data, loading, error }] = useCreateManyCoreViewFieldGroupsMutation({ + * variables: { + * inputs: // value for 'inputs' + * }, + * }); + */ +export function useCreateManyCoreViewFieldGroupsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateManyCoreViewFieldGroupsDocument, options); + } +export type CreateManyCoreViewFieldGroupsMutationHookResult = ReturnType; +export type CreateManyCoreViewFieldGroupsMutationResult = Apollo.MutationResult; +export type CreateManyCoreViewFieldGroupsMutationOptions = Apollo.BaseMutationOptions; export const CreateManyCoreViewFieldsDocument = gql` mutation CreateManyCoreViewFields($inputs: [CreateViewFieldInput!]!) { createManyCoreViewFields(inputs: $inputs) { @@ -14476,6 +14595,39 @@ export function useDeleteCoreViewFieldMutation(baseOptions?: Apollo.MutationHook export type DeleteCoreViewFieldMutationHookResult = ReturnType; export type DeleteCoreViewFieldMutationResult = Apollo.MutationResult; export type DeleteCoreViewFieldMutationOptions = Apollo.BaseMutationOptions; +export const DeleteCoreViewFieldGroupDocument = gql` + mutation DeleteCoreViewFieldGroup($input: DeleteViewFieldGroupInput!) { + deleteCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } +} + ${ViewFieldGroupFragmentFragmentDoc}`; +export type DeleteCoreViewFieldGroupMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteCoreViewFieldGroupMutation__ + * + * To run a mutation, you first call `useDeleteCoreViewFieldGroupMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteCoreViewFieldGroupMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteCoreViewFieldGroupMutation, { data, loading, error }] = useDeleteCoreViewFieldGroupMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useDeleteCoreViewFieldGroupMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteCoreViewFieldGroupDocument, options); + } +export type DeleteCoreViewFieldGroupMutationHookResult = ReturnType; +export type DeleteCoreViewFieldGroupMutationResult = Apollo.MutationResult; +export type DeleteCoreViewFieldGroupMutationOptions = Apollo.BaseMutationOptions; export const DeleteCoreViewFilterDocument = gql` mutation DeleteCoreViewFilter($input: DeleteViewFilterInput!) { deleteCoreViewFilter(input: $input) { @@ -14863,6 +15015,39 @@ export function useUpdateCoreViewFieldMutation(baseOptions?: Apollo.MutationHook export type UpdateCoreViewFieldMutationHookResult = ReturnType; export type UpdateCoreViewFieldMutationResult = Apollo.MutationResult; export type UpdateCoreViewFieldMutationOptions = Apollo.BaseMutationOptions; +export const UpdateCoreViewFieldGroupDocument = gql` + mutation UpdateCoreViewFieldGroup($input: UpdateViewFieldGroupInput!) { + updateCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } +} + ${ViewFieldGroupFragmentFragmentDoc}`; +export type UpdateCoreViewFieldGroupMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateCoreViewFieldGroupMutation__ + * + * To run a mutation, you first call `useUpdateCoreViewFieldGroupMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateCoreViewFieldGroupMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateCoreViewFieldGroupMutation, { data, loading, error }] = useUpdateCoreViewFieldGroupMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUpdateCoreViewFieldGroupMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateCoreViewFieldGroupDocument, options); + } +export type UpdateCoreViewFieldGroupMutationHookResult = ReturnType; +export type UpdateCoreViewFieldGroupMutationResult = Apollo.MutationResult; +export type UpdateCoreViewFieldGroupMutationOptions = Apollo.BaseMutationOptions; export const UpdateCoreViewFilterDocument = gql` mutation UpdateCoreViewFilter($input: UpdateViewFilterInput!) { updateCoreViewFilter(input: $input) { @@ -14998,8 +15183,8 @@ export type UpdateCoreViewSortMutationHookResult = ReturnType; export type UpdateCoreViewSortMutationOptions = Apollo.BaseMutationOptions; export const FindAllCoreViewsDocument = gql` - query FindAllCoreViews { - getCoreViews { + query FindAllCoreViews($viewTypes: [ViewType!]) { + getCoreViews(viewTypes: $viewTypes) { ...ViewFragment } } @@ -15017,6 +15202,7 @@ export const FindAllCoreViewsDocument = gql` * @example * const { data, loading, error } = useFindAllCoreViewsQuery({ * variables: { + * viewTypes: // value for 'viewTypes' * }, * }); */ @@ -15031,6 +15217,41 @@ export function useFindAllCoreViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookO export type FindAllCoreViewsQueryHookResult = ReturnType; export type FindAllCoreViewsLazyQueryHookResult = ReturnType; export type FindAllCoreViewsQueryResult = Apollo.QueryResult; +export const FindFieldsWidgetCoreViewsDocument = gql` + query FindFieldsWidgetCoreViews($viewTypes: [ViewType!]) { + getCoreViews(viewTypes: $viewTypes) { + ...ViewFragment + } +} + ${ViewFragmentFragmentDoc}`; + +/** + * __useFindFieldsWidgetCoreViewsQuery__ + * + * To run a query within a React component, call `useFindFieldsWidgetCoreViewsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindFieldsWidgetCoreViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useFindFieldsWidgetCoreViewsQuery({ + * variables: { + * viewTypes: // value for 'viewTypes' + * }, + * }); + */ +export function useFindFieldsWidgetCoreViewsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindFieldsWidgetCoreViewsDocument, options); + } +export function useFindFieldsWidgetCoreViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindFieldsWidgetCoreViewsDocument, options); + } +export type FindFieldsWidgetCoreViewsQueryHookResult = ReturnType; +export type FindFieldsWidgetCoreViewsLazyQueryHookResult = ReturnType; +export type FindFieldsWidgetCoreViewsQueryResult = Apollo.QueryResult; export const FindManyCoreViewFieldsDocument = gql` query FindManyCoreViewFields($viewId: String!) { getCoreViewFields(viewId: $viewId) { @@ -15207,8 +15428,8 @@ export type FindManyCoreViewSortsQueryHookResult = ReturnType; export type FindManyCoreViewSortsQueryResult = Apollo.QueryResult; export const FindManyCoreViewsDocument = gql` - query FindManyCoreViews($objectMetadataId: String) { - getCoreViews(objectMetadataId: $objectMetadataId) { + query FindManyCoreViews($objectMetadataId: String, $viewTypes: [ViewType!]) { + getCoreViews(objectMetadataId: $objectMetadataId, viewTypes: $viewTypes) { ...ViewFragment } } @@ -15227,6 +15448,7 @@ export const FindManyCoreViewsDocument = gql` * const { data, loading, error } = useFindManyCoreViewsQuery({ * variables: { * objectMetadataId: // value for 'objectMetadataId' + * viewTypes: // value for 'viewTypes' * }, * }); */ diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx index 460194ba54..45a8936860 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx @@ -1,5 +1,4 @@ import { Action } from '@/action-menu/actions/components/Action'; -import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; @@ -7,12 +6,9 @@ import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hook import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; export const CancelRecordPageLayoutSingleRecordAction = () => { - const recordId = useSelectedRecordIdOrThrow(); - const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ - id: recordId, targetObjectNameSingular: objectMetadataItem.nameSingular, }); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx index 4ee4230a44..3aff9fcb9b 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx @@ -1,17 +1,13 @@ import { Action } from '@/action-menu/actions/components/Action'; -import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; import { useResetLocationHash } from 'twenty-ui/utilities'; export const EditRecordPageLayoutSingleRecordAction = () => { - const recordId = useSelectedRecordIdOrThrow(); - const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ - id: recordId, targetObjectNameSingular: objectMetadataItem.nameSingular, }); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx index 37cf36cb4e..322e5296f9 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx @@ -1,22 +1,22 @@ import { Action } from '@/action-menu/actions/components/Action'; -import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; +import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups'; import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout'; import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; export const SaveRecordPageLayoutSingleRecordAction = () => { - const recordId = useSelectedRecordIdOrThrow(); - const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ - id: recordId, targetObjectNameSingular: objectMetadataItem.nameSingular, }); const { savePageLayout } = useSavePageLayout(pageLayoutId); + const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups({ + pageLayoutId, + }); const { setIsPageLayoutInEditMode } = useSetIsPageLayoutInEditMode(pageLayoutId); @@ -27,6 +27,8 @@ export const SaveRecordPageLayoutSingleRecordAction = () => { const result = await savePageLayout(); if (result.status === 'successful') { + await saveFieldsWidgetGroups(); + closeCommandMenu(); setIsPageLayoutInEditMode(false); } diff --git a/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts b/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts index 65e92a91db..07b2440f84 100644 --- a/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts +++ b/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts @@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState'; import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState'; +import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState'; import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; @@ -69,6 +70,13 @@ export const useExecuteTasksOnAnyLocationChange = () => { false, ); + set( + hasInitializedFieldsWidgetGroupsDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + {}, + ); + set(currentPageLayoutIdState, null); } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx index 0fafed4278..6c024cc551 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx @@ -3,12 +3,15 @@ import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/co import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord'; import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor'; +import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; -import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { + type FieldsConfiguration, + WidgetConfigurationType, +} from '~/generated-metadata/graphql'; const StyledOuterContainer = styled.div` display: flex; @@ -28,25 +31,23 @@ const StyledContainer = styled.div` export const CommandMenuPageLayoutFieldsLayout = () => { const { goBackFromCommandMenu } = useCommandMenuHistory(); - const { pageLayoutId, objectNameSingular } = + const { pageLayoutId } = usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord(); const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); - const defaultFieldsConfiguration = - useTemporaryFieldsConfiguration(objectNameSingular); - const [fieldsConfiguration, setFieldsConfiguration] = - useState(defaultFieldsConfiguration); + const temporaryFieldsConfiguration = useTemporaryFieldsConfiguration(); if (!isDefined(widgetInEditMode)) { return null; } - const handleConfigurationChange = ( - updatedConfiguration: FieldsConfiguration, - ) => { - // TODO: replace with a call to updatePageLayoutWidget - setFieldsConfiguration(updatedConfiguration); - }; + const widgetConfiguration = widgetInEditMode.configuration; + + const fieldsConfiguration: FieldsConfiguration = + isDefined(widgetConfiguration) && + widgetConfiguration.configurationType === WidgetConfigurationType.FIELDS + ? (widgetConfiguration as FieldsConfiguration) + : temporaryFieldsConfiguration; return ( @@ -55,9 +56,14 @@ export const CommandMenuPageLayoutFieldsLayout = () => { onBackClick={goBackFromCommandMenu} /> + diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx index 2ad8f8c293..fbe96d4951 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx @@ -6,12 +6,13 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord'; import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; +import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { IconLayoutSidebarRight } from 'twenty-ui/display'; +import { type FieldsConfiguration } from '~/generated-metadata/graphql'; const StyledContainer = styled.div` display: flex; @@ -33,15 +34,22 @@ export const CommandMenuPageLayoutFieldsSettings = () => { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord(); const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); - const fieldsConfiguration = - useTemporaryFieldsConfiguration(objectNameSingular); + + const fieldsConfiguration = widgetInEditMode?.configuration as + | FieldsConfiguration + | undefined; + + const { groups } = useFieldsWidgetGroups({ + viewId: fieldsConfiguration?.viewId ?? null, + objectNameSingular, + }); if (!isDefined(widgetInEditMode)) { return null; } - const totalFieldsCount = fieldsConfiguration.sections.reduce( - (count, section) => count + section.fields.length, + const totalFieldsCount = groups.reduce( + (count, group) => count + group.fields.length, 0, ); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx index 9a622123e1..facc74a409 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDateGranularitySelectionDropdownContent.tsx @@ -5,7 +5,6 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ import { getDateGranularityLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityLabel'; import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType'; import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; @@ -17,7 +16,10 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useR import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { MenuItemSelect } from 'twenty-ui/navigation'; -import { type WidgetConfiguration } from '~/generated-metadata/graphql'; +import { + type FieldsConfiguration, + type WidgetConfiguration, +} from '~/generated-metadata/graphql'; type ChartDateGranularitySelectionDropdownContentProps = { axis?: 'primary' | 'secondary'; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts index 5ed470a0a9..a7b7bae016 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts @@ -32,10 +32,7 @@ export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord = throw new Error('Only one record should be selected'); } - const recordId: string = targetedRecordsRule.selectedRecordIds[0]; - const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ - id: recordId, targetObjectNameSingular: objectMetadataItem.nameSingular, }); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isChartWidget.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isChartWidget.ts index 351e88bcde..dc1b341737 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isChartWidget.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isChartWidget.ts @@ -1,13 +1,9 @@ import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget'; import { isWidgetConfigurationOfTypeGraph } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; -import { type WidgetConfiguration } from '~/generated-metadata/graphql'; export const isChartWidget = ( pageLayoutWidget: PageLayoutWidget, ): pageLayoutWidget is ChartWidget => { - return isWidgetConfigurationOfTypeGraph( - // TODO: Remove this cast when we FieldsConfiguration and FieldConfiguration are in the backend - pageLayoutWidget.configuration as WidgetConfiguration, - ); + return isWidgetConfigurationOfTypeGraph(pageLayoutWidget.configuration); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType.ts index c85de34a17..19ed82902d 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType.ts @@ -1,11 +1,11 @@ import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; import { type AggregateChartConfiguration, type BarChartConfiguration, type CalendarConfiguration, type EmailsConfiguration, type FieldRichTextConfiguration, + type FieldsConfiguration, type FilesConfiguration, type FrontComponentConfiguration, type GaugeChartConfiguration, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph.ts index d600af3836..5da5e66a8a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph.ts @@ -1,8 +1,10 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType'; import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; -import { type WidgetConfiguration } from '~/generated-metadata/graphql'; +import { + type FieldsConfiguration, + type WidgetConfiguration, +} from '~/generated-metadata/graphql'; export const isWidgetConfigurationOfTypeGraph = ( configuration: diff --git a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts index 99c525d9c5..bc87be22d7 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts @@ -50,13 +50,15 @@ const renderHooks = ({ isCompact: false, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, viewFields: [], + viewFieldGroups: [], viewGroups: [], viewSorts: [], + viewFilters: [], + viewFilterGroups: [], kanbanAggregateOperation: AggregateOperations.COUNT, icon: '', kanbanAggregateOperationFieldMetadataId: '', position: 0, - viewFilters: [], visibility: ViewVisibility.WORKSPACE, createdByUserWorkspaceId: null, shouldHideEmptyGroups: false, diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index e1eb9b3dde..ddf9c2323c 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -149,6 +149,9 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { updateCurrentViewParams.mainGroupByFieldMetadataId = null; return await updateCurrentView(updateCurrentViewParams); } + case ViewType.FieldsWidget: { + return; + } default: { return assertUnreachable(viewType); } diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/PageLayoutRecordPageRenderer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/PageLayoutRecordPageRenderer.tsx index 55caf67890..ba3c1aca4a 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/PageLayoutRecordPageRenderer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/PageLayoutRecordPageRenderer.tsx @@ -6,7 +6,7 @@ import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-r import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer'; -import { useRecordPageLayoutId } from '@/page-layout/hooks/useRecordPageLayoutId'; +import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdForRecord'; import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext'; import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter'; @@ -50,7 +50,7 @@ export const PageLayoutRecordPageRenderer = ({ }), ); - const { pageLayoutId } = useRecordPageLayoutId({ + const { pageLayoutId } = usePageLayoutIdForRecord({ id: targetRecordIdentifier.id, targetObjectNameSingular: targetRecordIdentifier.targetObjectNameSingular, }); diff --git a/packages/twenty-front/src/modules/page-layout/components/utils/__tests__/getPageLayoutVerticalListViewerVariant.test.ts b/packages/twenty-front/src/modules/page-layout/components/utils/__tests__/getPageLayoutVerticalListViewerVariant.test.ts new file mode 100644 index 0000000000..4d4cbdc634 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/utils/__tests__/getPageLayoutVerticalListViewerVariant.test.ts @@ -0,0 +1,43 @@ +import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant'; + +describe('getPageLayoutVerticalListViewerVariant', () => { + it('should return side-column when isInPinnedTab is true', () => { + expect( + getPageLayoutVerticalListViewerVariant({ + isInPinnedTab: true, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('side-column'); + }); + + it('should return side-column when isMobile is true', () => { + expect( + getPageLayoutVerticalListViewerVariant({ + isInPinnedTab: false, + isMobile: true, + isInRightDrawer: false, + }), + ).toBe('side-column'); + }); + + it('should return side-column when isInRightDrawer is true', () => { + expect( + getPageLayoutVerticalListViewerVariant({ + isInPinnedTab: false, + isMobile: false, + isInRightDrawer: true, + }), + ).toBe('side-column'); + }); + + it('should return default when none of the conditions are true', () => { + expect( + getPageLayoutVerticalListViewerVariant({ + isInPinnedTab: false, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('default'); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts index 4791360476..93c24a353a 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts @@ -47,7 +47,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -85,7 +85,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -123,7 +123,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -161,7 +161,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -199,7 +199,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -237,7 +237,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -275,7 +275,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts index fc49726a88..ff80d7e341 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -48,8 +49,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { // a configuration with actual field metadata IDs from the backend. configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -71,8 +72,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -116,8 +117,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -161,8 +162,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -199,8 +200,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultOpportunityRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultOpportunityRecordPageLayout.ts index 3437dfffb1..674e9763cd 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultOpportunityRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultOpportunityRecordPageLayout.ts @@ -47,7 +47,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -85,7 +85,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -123,7 +123,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -161,7 +161,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -199,7 +199,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -237,7 +237,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -275,7 +275,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultPersonRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultPersonRecordPageLayout.ts index defdfa7812..4798013b6c 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultPersonRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultPersonRecordPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -45,8 +46,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -83,8 +84,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -121,8 +122,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -159,8 +160,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -197,8 +198,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -235,8 +236,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -273,8 +274,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultRecordPageLayout.ts index a9f84e73ad..85deb2dd52 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultRecordPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -45,8 +46,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -83,8 +84,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -121,8 +122,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -159,8 +160,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -197,8 +198,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts index 1426ad61d4..89157e87a5 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultTaskRecordPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -45,8 +46,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -68,8 +69,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -113,8 +114,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -158,8 +159,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -196,8 +197,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowPageLayout.ts index 219aadd800..030ad4ca2b 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -50,8 +51,8 @@ export const DEFAULT_WORKFLOW_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts index 78b33ec78f..f8c5476ada 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -51,8 +52,8 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -90,8 +91,8 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts index 38dea76f35..4e0e8ee35d 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts @@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout'; import { PageLayoutTabLayoutMode, PageLayoutType, + WidgetConfigurationType, WidgetType, } from '~/generated-metadata/graphql'; @@ -51,8 +52,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -90,8 +91,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index c72431fd4f..daa09cc42e 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -165,6 +165,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` } ... on FieldsConfiguration { configurationType + viewId } ... on FilesConfiguration { configurationType diff --git a/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts b/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts index fe0df680a8..bd6fcaf3f3 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/mutations/updatePageLayoutWithTabsAndWidgets.ts @@ -1,9 +1,8 @@ +import { PAGE_LAYOUT_TAB_FRAGMENT } from '@/dashboards/graphql/fragments/pageLayoutTabFragment'; import { gql } from '@apollo/client'; -import { PAGE_LAYOUT_WIDGET_FRAGMENT } from '@/page-layout/graphql/fragments/pageLayoutWidgetFragment'; - export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql` - ${PAGE_LAYOUT_WIDGET_FRAGMENT} + ${PAGE_LAYOUT_TAB_FRAGMENT} mutation UpdatePageLayoutWithTabsAndWidgets( $id: String! $input: UpdatePageLayoutWithTabsInput! @@ -13,20 +12,12 @@ export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql` name type objectMetadataId + defaultTabToFocusOnMobileAndSidePanelId createdAt updatedAt deletedAt tabs { - id - applicationId - title - position - pageLayoutId - widgets { - ...PageLayoutWidgetFragment - } - createdAt - updatedAt + ...PageLayoutTabFragment } } } diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx index 1690b5858f..e4ed172f3f 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx @@ -60,7 +60,7 @@ describe('usePageLayoutWithRelationWidgets', () => { configuration: { __typename: 'FieldsConfiguration', configurationType: WidgetConfigurationType.FIELDS, - sections: [], + viewId: null, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutIdForRecord.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutIdForRecord.ts new file mode 100644 index 0000000000..5cb4821298 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutIdForRecord.ts @@ -0,0 +1,51 @@ +import { useRecoilValue } from 'recoil'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector'; +import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId'; +import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; +import { isDefined } from 'twenty-shared/utils'; + +export const usePageLayoutIdForRecord = ({ + id, + targetObjectNameSingular, +}: TargetRecordIdentifier) => { + const isDashboard = + targetObjectNameSingular === CoreObjectNameSingular.Dashboard; + + const { record } = useFindOneRecord( + { + objectNameSingular: targetObjectNameSingular, + objectRecordId: id, + withSoftDeleted: true, + skip: !isDashboard, + }, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: targetObjectNameSingular, + }); + + const recordPageLayout = useRecoilValue( + recordPageLayoutByObjectMetadataIdFamilySelector({ + objectMetadataId: objectMetadataItem.id, + }), + ); + + if (isDashboard) { + return { + pageLayoutId: record?.pageLayoutId ?? null, + }; + } + + const pageLayoutId = isDefined(recordPageLayout) + ? recordPageLayout.id + : getDefaultRecordPageLayoutId({ targetObjectNameSingular }); + + return { + pageLayoutId, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts deleted file mode 100644 index 0c2a403cec..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId'; -import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; - -export const useRecordPageLayoutId = ({ - id, - targetObjectNameSingular, -}: TargetRecordIdentifier) => { - const { record } = useFindOneRecord( - { - objectNameSingular: targetObjectNameSingular, - objectRecordId: id, - withSoftDeleted: true, - }, - ); - - const pageLayoutId = getRecordPageLayoutId({ - record, - targetObjectNameSingular, - }); - - return { - pageLayoutId, - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts index 373a6f3e33..e641ba3c99 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts @@ -1,27 +1,26 @@ -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector'; +import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId'; import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({ - id, targetObjectNameSingular, -}: TargetRecordIdentifier) => { - const record = useRecoilValue(recordStoreFamilyState(id)); - - if (!isDefined(record)) { - throw new Error(`Record with id ${id} not found in record store`); - } - - const pageLayoutId = getRecordPageLayoutId({ - record, - targetObjectNameSingular, +}: Omit) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: targetObjectNameSingular, }); - if (!isDefined(pageLayoutId)) { - throw new Error(`Page layout id not found for record with id ${id}`); - } + const recordPageLayout = useRecoilValue( + recordPageLayoutByObjectMetadataIdFamilySelector({ + objectMetadataId: objectMetadataItem.id, + }), + ); + + const pageLayoutId = isDefined(recordPageLayout) + ? recordPageLayout.id + : getDefaultRecordPageLayoutId({ targetObjectNameSingular }); return { pageLayoutId, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts index 1edb8a96d8..6ff41fb7e4 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts @@ -1,4 +1,6 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; @@ -42,6 +44,16 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( instanceId: tabListComponentInstanceId, }); + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + componentInstanceId, + ); + + const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState( + fieldsWidgetGroupsPersistedComponentState, + componentInstanceId, + ); + const resetDraftPageLayoutToPersistedPageLayout = useRecoilCallback( ({ set, snapshot }) => () => { @@ -73,12 +85,19 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted); set(pageLayoutCurrentLayoutsState, tabLayouts); + + const fieldsWidgetGroupsPersisted = snapshot + .getLoadable(fieldsWidgetGroupsPersistedState) + .getValue(); + set(fieldsWidgetGroupsDraftState, fieldsWidgetGroupsPersisted); } }, [ pageLayoutDraftState, pageLayoutPersistedState, pageLayoutCurrentLayoutsState, + fieldsWidgetGroupsDraftState, + fieldsWidgetGroupsPersistedState, activeTabIdAtom, store, ], diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts new file mode 100644 index 0000000000..c951b18f33 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts @@ -0,0 +1,185 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { computeFieldsWidgetFieldDiff } from '@/page-layout/utils/computeFieldsWidgetFieldDiff'; +import { computeFieldsWidgetGroupDiff } from '@/page-layout/utils/computeFieldsWidgetGroupDiff'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; +import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; +import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { + type FieldsConfiguration, + WidgetConfigurationType, +} from '~/generated-metadata/graphql'; + +type UseSaveFieldsWidgetGroupsParams = { + pageLayoutId: string; +}; + +export const useSaveFieldsWidgetGroups = ({ + pageLayoutId, +}: UseSaveFieldsWidgetGroupsParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState( + fieldsWidgetGroupsPersistedComponentState, + pageLayoutId, + ); + + const pageLayoutPersistedState = useRecoilComponentCallbackState( + pageLayoutPersistedComponentState, + pageLayoutId, + ); + + const { + performViewFieldGroupAPICreate, + performViewFieldGroupAPIUpdate, + performViewFieldGroupAPIDelete, + } = usePerformViewFieldGroupAPIPersist(); + + const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist(); + + const { refreshAllCoreViews } = useRefreshAllCoreViews(); + + const getViewIdForWidget = useRecoilCallback( + ({ snapshot }) => + (widgetId: string): string | null => { + const pageLayoutPersisted = snapshot + .getLoadable(pageLayoutPersistedState) + .getValue(); + + if (!isDefined(pageLayoutPersisted)) { + return null; + } + + for (const tab of pageLayoutPersisted.tabs) { + for (const widget of tab.widgets) { + if ( + widget.id === widgetId && + isDefined(widget.configuration) && + widget.configuration.configurationType === + WidgetConfigurationType.FIELDS + ) { + return ( + (widget.configuration as FieldsConfiguration).viewId ?? null + ); + } + } + } + + return null; + }, + [pageLayoutPersistedState], + ); + + const saveFieldsWidgetGroups = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const allDraftGroups = snapshot + .getLoadable(fieldsWidgetGroupsDraftState) + .getValue(); + const allPersistedGroups = snapshot + .getLoadable(fieldsWidgetGroupsPersistedState) + .getValue(); + + const widgetIds = new Set([ + ...Object.keys(allDraftGroups), + ...Object.keys(allPersistedGroups), + ]); + + for (const widgetId of widgetIds) { + const draftGroups = allDraftGroups[widgetId] ?? []; + const persistedGroups = allPersistedGroups[widgetId] ?? []; + + if (draftGroups.length === 0 && persistedGroups.length === 0) { + continue; + } + + const viewId = getViewIdForWidget(widgetId); + + if (!isDefined(viewId)) { + continue; + } + + const { createdGroups, deletedGroups, updatedGroups } = + computeFieldsWidgetGroupDiff(persistedGroups, draftGroups); + + if (createdGroups.length > 0) { + await performViewFieldGroupAPICreate({ + inputs: createdGroups.map((group) => ({ + id: group.id, + name: group.name, + position: group.position, + isVisible: group.isVisible, + viewId, + })), + }); + } + + if (deletedGroups.length > 0) { + for (const group of deletedGroups) { + await performViewFieldGroupAPIDelete([ + { input: { id: group.id } }, + ]); + } + } + + if (updatedGroups.length > 0) { + const updates = updatedGroups.map((group) => ({ + input: { + id: group.id, + update: { + name: group.name, + position: group.position, + isVisible: group.isVisible, + }, + }, + })); + + await performViewFieldGroupAPIUpdate(updates); + } + + const fieldUpdates = computeFieldsWidgetFieldDiff( + persistedGroups, + draftGroups, + ); + + if (fieldUpdates.length > 0) { + const viewFieldUpdateInputs = fieldUpdates.map( + ({ viewFieldId, ...updates }) => ({ + input: { + id: viewFieldId, + update: updates, + }, + }), + ); + + await performViewFieldAPIUpdate(viewFieldUpdateInputs); + } + } + + set(fieldsWidgetGroupsPersistedState, allDraftGroups); + + await refreshAllCoreViews(); + + return { status: 'successful' as const }; + }, + [ + fieldsWidgetGroupsDraftState, + fieldsWidgetGroupsPersistedState, + getViewIdForWidget, + performViewFieldGroupAPICreate, + performViewFieldGroupAPIDelete, + performViewFieldGroupAPIUpdate, + performViewFieldAPIUpdate, + refreshAllCoreViews, + ], + ); + + return { saveFieldsWidgetGroups }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts index 96d7451638..4e32f90abe 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts @@ -6,11 +6,13 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo import { type PageLayout } from '@/page-layout/types/PageLayout'; import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; +import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft'; import { transformPageLayout } from '@/page-layout/utils/transformPageLayout'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { PageLayoutType } from '~/generated-metadata/graphql'; export const useSavePageLayout = (pageLayoutIdFromProps: string) => { const pageLayoutId = useAvailableComponentInstanceIdOrThrow( @@ -55,9 +57,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { result.response.data?.updatePageLayoutWithTabsAndWidgets; if (isDefined(updatedPageLayout)) { - const pageLayoutToPersist: PageLayout = + const serverLayout: PageLayout = transformPageLayout(updatedPageLayout); + const pageLayoutToPersist = + serverLayout.type === PageLayoutType.RECORD_PAGE + ? reInjectDynamicRelationWidgetsFromDraft( + serverLayout, + pageLayoutDraft, + ) + : serverLayout; + set(pageLayoutPersistedCallbackState, pageLayoutToPersist); set( pageLayoutCurrentLayoutsCallbackState, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts index 3565e07bf3..cb48a01be4 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts @@ -1,80 +1,12 @@ -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; -import { useLingui } from '@lingui/react/macro'; -import { useMemo } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { + type FieldsConfiguration, + WidgetConfigurationType, +} from '~/generated-metadata/graphql'; -export const useTemporaryFieldsConfiguration = ( - objectNameSingular: string, -): FieldsConfiguration => { - const { t } = useLingui(); - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const configuration = useMemo(() => { - if (!isDefined(objectMetadataItem)) { - throw new Error('Object metadata item is not defined'); - } - - const fieldsToDisplay = objectMetadataItem.fields; - - if (fieldsToDisplay.length === 0) { - throw new Error('No fields to display'); - } - - const generalFields: Array<{ fieldMetadataId: string; position: number }> = - []; - const otherFields: Array<{ fieldMetadataId: string; position: number }> = - []; - - let generalPosition = 0; - let otherPosition = 0; - - fieldsToDisplay.forEach((field) => { - if (field.isCustom === true) { - otherFields.push({ - fieldMetadataId: field.id, - position: otherPosition++, - }); - } else { - generalFields.push({ - fieldMetadataId: field.id, - position: generalPosition++, - }); - } - }); - - const sections = []; - - if (generalFields.length > 0) { - sections.push({ - id: `${objectNameSingular}-section-general`, - title: t`General`, - position: 0, - fields: generalFields, - }); - } - - if (otherFields.length > 0) { - sections.push({ - id: `${objectNameSingular}-section-other`, - title: t`Other`, - position: 1, - fields: otherFields, - }); - } - - if (sections.length === 0) { - throw new Error('No sections to display'); - } - - return { - __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections, - }; - }, [objectMetadataItem, objectNameSingular, t]); - - return configuration; +export const useTemporaryFieldsConfiguration = (): FieldsConfiguration => { + return { + __typename: 'FieldsConfiguration', + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, + }; }; diff --git a/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsDraftComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsDraftComponentState.ts new file mode 100644 index 0000000000..93ab19e09f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsDraftComponentState.ts @@ -0,0 +1,12 @@ +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const fieldsWidgetGroupsDraftComponentState = createComponentState< + Record +>({ + key: 'fieldsWidgetGroupsDraftComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsPersistedComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsPersistedComponentState.ts new file mode 100644 index 0000000000..3545b4b3a8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/fieldsWidgetGroupsPersistedComponentState.ts @@ -0,0 +1,12 @@ +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const fieldsWidgetGroupsPersistedComponentState = createComponentState< + Record +>({ + key: 'fieldsWidgetGroupsPersistedComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState.ts new file mode 100644 index 0000000000..02310f66e5 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState.ts @@ -0,0 +1,10 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const hasInitializedFieldsWidgetGroupsDraftComponentState = + createComponentState>({ + key: 'hasInitializedFieldsWidgetGroupsDraftComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector.ts b/packages/twenty-front/src/modules/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector.ts new file mode 100644 index 0000000000..534393bd42 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector.ts @@ -0,0 +1,19 @@ +import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; +import { selectorFamily } from 'recoil'; + +export const recordPageLayoutByObjectMetadataIdFamilySelector = selectorFamily< + PageLayout | undefined, + { objectMetadataId: string } +>({ + key: 'recordPageLayoutByObjectMetadataIdFamilySelector', + get: + ({ objectMetadataId }) => + ({ get }) => { + const recordPageLayouts = get(recordPageLayoutsState); + + return recordPageLayouts.find( + (pageLayout) => pageLayout.objectMetadataId === objectMetadataId, + ); + }, +}); diff --git a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts index 079c75340b..8f32248fff 100644 --- a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts @@ -1,5 +1,6 @@ import { type RulesLogic } from 'json-logic-js'; +// Legacy types - kept for temporary configuration fallback export type FieldsConfigurationFieldItem = { fieldMetadataId: string; position: number; @@ -8,15 +9,9 @@ export type FieldsConfigurationFieldItem = { isVisible?: boolean; }; -export type FieldsConfigurationSection = { +export type FieldsConfigurationGroup = { id: string; title: string; position: number; fields: FieldsConfigurationFieldItem[]; }; - -export type FieldsConfiguration = { - __typename: 'FieldsConfiguration'; - configurationType: 'FIELDS'; - sections: FieldsConfigurationSection[]; -}; diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts index 93384fe680..50faa6390d 100644 --- a/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts @@ -1,7 +1,7 @@ import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; import { type Nullable } from 'twenty-shared/types'; import { + type FieldsConfiguration, type PageLayoutWidget as PageLayoutWidgetGenerated, type WidgetConfiguration, } from '~/generated-metadata/graphql'; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetFieldDiff.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetFieldDiff.test.ts new file mode 100644 index 0000000000..af56e939b8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetFieldDiff.test.ts @@ -0,0 +1,337 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { computeFieldsWidgetFieldDiff } from '@/page-layout/utils/computeFieldsWidgetFieldDiff'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; + +const mockFieldMetadataItem = { + id: 'field-meta-1', + name: 'name', + label: 'Name', + type: 'TEXT', +} as FieldMetadataItem; + +const makeGroup = ( + overrides: Partial & { id: string }, +): FieldsWidgetGroup => ({ + name: 'Group', + position: 0, + isVisible: true, + fields: [], + ...overrides, +}); + +describe('computeFieldsWidgetFieldDiff', () => { + it('should return empty updates when both arrays are empty', () => { + const result = computeFieldsWidgetFieldDiff([], []); + + expect(result).toEqual([]); + }); + + it('should return empty updates when fields are identical', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(groups, groups); + + expect(result).toEqual([]); + }); + + it('should detect visibility changes', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: false, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([{ viewFieldId: 'vf1', isVisible: false }]); + }); + + it('should detect position changes', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 3, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([{ viewFieldId: 'vf1', position: 3 }]); + }); + + it('should detect group changes (field moved to different group)', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + makeGroup({ id: 'g2' }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1' }), + makeGroup({ + id: 'g2', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([{ viewFieldId: 'vf1', viewFieldGroupId: 'g2' }]); + }); + + it('should detect multiple simultaneous changes on a field', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + makeGroup({ id: 'g2' }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1' }), + makeGroup({ + id: 'g2', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 5, + isVisible: false, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([ + { + viewFieldId: 'vf1', + isVisible: false, + position: 5, + viewFieldGroupId: 'g2', + }, + ]); + }); + + it('should skip fields without viewFieldId', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + }, + ], + }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 5, + isVisible: false, + globalIndex: 0, + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([]); + }); + + it('should skip fields not found in persisted state', () => { + const persisted: FieldsWidgetGroup[] = [makeGroup({ id: 'g1' })]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf-new', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toEqual([]); + }); + + it('should handle multiple fields across multiple groups', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + { + fieldMetadataItem: mockFieldMetadataItem, + position: 1, + isVisible: true, + globalIndex: 1, + viewFieldId: 'vf2', + }, + ], + }), + makeGroup({ + id: 'g2', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: false, + globalIndex: 2, + viewFieldId: 'vf3', + }, + ], + }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + { + fieldMetadataItem: mockFieldMetadataItem, + position: 1, + isVisible: false, + globalIndex: 1, + viewFieldId: 'vf2', + }, + ], + }), + makeGroup({ + id: 'g2', + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 2, + viewFieldId: 'vf3', + }, + ], + }), + ]; + + const result = computeFieldsWidgetFieldDiff(persisted, draft); + + expect(result).toHaveLength(2); + expect(result).toContainEqual({ viewFieldId: 'vf2', isVisible: false }); + expect(result).toContainEqual({ viewFieldId: 'vf3', isVisible: true }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetGroupDiff.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetGroupDiff.test.ts new file mode 100644 index 0000000000..a686e9f551 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/computeFieldsWidgetGroupDiff.test.ts @@ -0,0 +1,191 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { computeFieldsWidgetGroupDiff } from '@/page-layout/utils/computeFieldsWidgetGroupDiff'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; + +const mockFieldMetadataItem = { + id: 'field-meta-1', + name: 'name', + label: 'Name', + type: 'TEXT', +} as FieldMetadataItem; + +const makeGroup = ( + overrides: Partial & { id: string }, +): FieldsWidgetGroup => ({ + name: 'Group', + position: 0, + isVisible: true, + fields: [], + ...overrides, +}); + +describe('computeFieldsWidgetGroupDiff', () => { + it('should return empty diffs when both arrays are empty', () => { + const result = computeFieldsWidgetGroupDiff([], []); + + expect(result).toEqual({ + createdGroups: [], + deletedGroups: [], + updatedGroups: [], + }); + }); + + it('should return empty diffs when groups are identical', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + makeGroup({ id: 'g2', name: 'Group 2', position: 1 }), + ]; + + const result = computeFieldsWidgetGroupDiff(groups, groups); + + expect(result).toEqual({ + createdGroups: [], + deletedGroups: [], + updatedGroups: [], + }); + }); + + it('should detect created groups', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + makeGroup({ id: 'g2', name: 'New Group', position: 1 }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.createdGroups).toHaveLength(1); + expect(result.createdGroups[0].id).toBe('g2'); + expect(result.deletedGroups).toHaveLength(0); + expect(result.updatedGroups).toHaveLength(0); + }); + + it('should detect deleted groups', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + makeGroup({ id: 'g2', name: 'Group 2', position: 1 }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.createdGroups).toHaveLength(0); + expect(result.deletedGroups).toHaveLength(1); + expect(result.deletedGroups[0].id).toBe('g2'); + expect(result.updatedGroups).toHaveLength(0); + }); + + it('should detect updated groups when name changes', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Old Name', position: 0 }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'New Name', position: 0 }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.updatedGroups).toHaveLength(1); + expect(result.updatedGroups[0].name).toBe('New Name'); + }); + + it('should detect updated groups when position changes', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 5 }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.updatedGroups).toHaveLength(1); + expect(result.updatedGroups[0].position).toBe(5); + }); + + it('should detect updated groups when visibility changes', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', isVisible: true }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', isVisible: false }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.updatedGroups).toHaveLength(1); + expect(result.updatedGroups[0].isVisible).toBe(false); + }); + + it('should handle simultaneous creates, deletes, and updates', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + makeGroup({ id: 'g2', name: 'To Delete', position: 1 }), + makeGroup({ id: 'g3', name: 'To Update', position: 2 }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ id: 'g1', name: 'Group 1', position: 0 }), + makeGroup({ id: 'g3', name: 'Updated Name', position: 2 }), + makeGroup({ id: 'g4', name: 'New Group', position: 3 }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.createdGroups).toHaveLength(1); + expect(result.createdGroups[0].id).toBe('g4'); + expect(result.deletedGroups).toHaveLength(1); + expect(result.deletedGroups[0].id).toBe('g2'); + expect(result.updatedGroups).toHaveLength(1); + expect(result.updatedGroups[0].id).toBe('g3'); + }); + + it('should not mark group as updated when fields differ but group props are same', () => { + const persisted: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + name: 'Group 1', + position: 0, + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const draft: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + name: 'Group 1', + position: 0, + fields: [ + { + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: false, + globalIndex: 0, + viewFieldId: 'vf1', + }, + ], + }), + ]; + + const result = computeFieldsWidgetGroupDiff(persisted, draft); + + expect(result.updatedGroups).toHaveLength(0); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts new file mode 100644 index 0000000000..88cf8c214a --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts @@ -0,0 +1,130 @@ +import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput'; +import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget'; +import { PageLayoutType, WidgetType } from '~/generated-metadata/graphql'; + +const makeWidget = ( + overrides: Partial & { id: string }, +): PageLayoutWidget => + ({ + title: 'Widget', + type: WidgetType.FIELDS, + pageLayoutTabId: 'tab-1', + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: null, + objectMetadataId: null, + ...overrides, + }) as PageLayoutWidget; + +const makeDraft = (tabs: DraftPageLayout['tabs']): DraftPageLayout => + ({ + id: 'layout-1', + name: 'Layout', + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: 'obj-1', + tabs, + }) as DraftPageLayout; + +const makeTab = ( + id: string, + widgets: PageLayoutWidget[], +): DraftPageLayout['tabs'][number] => + ({ + id, + title: 'Tab', + position: 0, + pageLayoutId: 'layout-1', + applicationId: 'app-1', + widgets, + }) as DraftPageLayout['tabs'][number]; + +describe('convertPageLayoutDraftToUpdateInput', () => { + it('should convert a draft layout to update input', () => { + const widget = makeWidget({ id: 'w1' }); + const draft = makeDraft([makeTab('tab-1', [widget])]); + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.name).toBe('Layout'); + expect(result.type).toBe(PageLayoutType.RECORD_PAGE); + expect(result.objectMetadataId).toBe('obj-1'); + expect(result.tabs).toHaveLength(1); + expect(result.tabs[0].widgets).toHaveLength(1); + 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, + }); + + const draft = makeDraft([makeTab('tab-1', [regularWidget, dynamicWidget])]); + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.tabs[0].widgets).toHaveLength(1); + expect(result.tabs[0].widgets[0].id).toBe('w1'); + }); + + it('should map gridPosition correctly', () => { + const widget = makeWidget({ + id: 'w1', + gridPosition: { row: 2, column: 3, rowSpan: 4, columnSpan: 5 }, + }); + const draft = makeDraft([makeTab('tab-1', [widget])]); + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.tabs[0].widgets[0].gridPosition).toEqual({ + row: 2, + column: 3, + rowSpan: 4, + columnSpan: 5, + }); + }); + + it('should handle objectMetadataId as null when not provided', () => { + const draft = makeDraft([makeTab('tab-1', [])]); + draft.objectMetadataId = undefined; + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.objectMetadataId).toBeNull(); + }); + + it('should handle multiple tabs', () => { + const draft = makeDraft([ + makeTab('tab-1', [makeWidget({ id: 'w1' })]), + makeTab('tab-2', [makeWidget({ id: 'w2' })]), + ]); + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.tabs).toHaveLength(2); + expect(result.tabs[0].id).toBe('tab-1'); + expect(result.tabs[1].id).toBe('tab-2'); + }); + + it('should keep all non-dynamic 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 draft = makeDraft([ + makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]), + ]); + + const result = convertPageLayoutDraftToUpdateInput(draft); + + expect(result.tabs[0].widgets).toHaveLength(2); + expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/filterVisibleWidgets.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/filterVisibleWidgets.test.ts index 97b8ae02da..3de219c436 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/filterVisibleWidgets.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/filterVisibleWidgets.test.ts @@ -1,6 +1,9 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; -import { WidgetType } from '~/generated-metadata/graphql'; import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets'; +import { + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; describe('filterVisibleWidgets', () => { const createMockWidget = ( @@ -22,8 +25,8 @@ describe('filterVisibleWidgets', () => { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultRecordPageLayoutId.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultRecordPageLayoutId.test.ts new file mode 100644 index 0000000000..12b6e42871 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultRecordPageLayoutId.test.ts @@ -0,0 +1,85 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId'; +import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId'; +import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId'; +import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId'; +import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId'; +import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId'; +import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId'; +import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId'; +import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId'; +import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId'; + +describe('getDefaultRecordPageLayoutId', () => { + it('should return Company layout id for Company object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Company, + }), + ).toBe(DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID); + }); + + it('should return Person layout id for Person object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Person, + }), + ).toBe(DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID); + }); + + it('should return Opportunity layout id for Opportunity object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Opportunity, + }), + ).toBe(DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID); + }); + + it('should return Note layout id for Note object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Note, + }), + ).toBe(DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID); + }); + + it('should return Task layout id for Task object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Task, + }), + ).toBe(DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID); + }); + + it('should return Workflow layout id for Workflow object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.Workflow, + }), + ).toBe(DEFAULT_WORKFLOW_PAGE_LAYOUT_ID); + }); + + it('should return WorkflowVersion layout id for WorkflowVersion object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }), + ).toBe(DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID); + }); + + it('should return WorkflowRun layout id for WorkflowRun object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: CoreObjectNameSingular.WorkflowRun, + }), + ).toBe(DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID); + }); + + it('should return default record page layout id for unknown object', () => { + expect( + getDefaultRecordPageLayoutId({ + targetObjectNameSingular: 'customObject', + }), + ).toBe(DEFAULT_RECORD_PAGE_LAYOUT_ID); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsWithVisibleWidgets.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsWithVisibleWidgets.test.ts index b43eac6f20..5bd5bc7c03 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsWithVisibleWidgets.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsWithVisibleWidgets.test.ts @@ -1,6 +1,9 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; -import { WidgetType } from '~/generated-metadata/graphql'; import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets'; +import { + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; describe('getTabsWithVisibleWidgets', () => { const createMockWidget = ( @@ -22,8 +25,8 @@ describe('getTabsWithVisibleWidgets', () => { }, configuration: { __typename: 'FieldsConfiguration', - configurationType: 'FIELDS', - sections: [], + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/isDynamicRelationWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/isDynamicRelationWidget.test.ts new file mode 100644 index 0000000000..9247f5d535 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/isDynamicRelationWidget.test.ts @@ -0,0 +1,43 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { + DYNAMIC_RELATION_WIDGET_ID_PREFIX, + isDynamicRelationWidget, +} from '@/page-layout/utils/isDynamicRelationWidget'; + +const makeWidget = (id: string): PageLayoutWidget => + ({ + id, + title: 'Widget', + type: 'FIELDS', + pageLayoutTabId: 'tab-1', + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: {}, + }) as PageLayoutWidget; + +describe('isDynamicRelationWidget', () => { + it('should return true for a widget with the dynamic relation prefix', () => { + const widget = makeWidget( + `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}some-relation`, + ); + + expect(isDynamicRelationWidget(widget)).toBe(true); + }); + + it('should return false for a widget without the dynamic relation prefix', () => { + const widget = makeWidget('regular-widget-id'); + + expect(isDynamicRelationWidget(widget)).toBe(false); + }); + + it('should return false for a widget with a similar but different prefix', () => { + const widget = makeWidget('dynamic-relation-other-widget-123'); + + expect(isDynamicRelationWidget(widget)).toBe(false); + }); + + it('should return true for the prefix alone as id', () => { + const widget = makeWidget(DYNAMIC_RELATION_WIDGET_ID_PREFIX); + + expect(isDynamicRelationWidget(widget)).toBe(true); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/reInjectDynamicRelationWidgetsFromDraft.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/reInjectDynamicRelationWidgetsFromDraft.test.ts new file mode 100644 index 0000000000..0182f5896e --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/reInjectDynamicRelationWidgetsFromDraft.test.ts @@ -0,0 +1,175 @@ +import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget'; +import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft'; +import { WidgetType } from '~/generated-metadata/graphql'; + +const makeWidget = ( + overrides: Partial & { id: string }, +): PageLayoutWidget => + ({ + title: 'Widget', + type: WidgetType.FIELDS, + pageLayoutTabId: 'tab-1', + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: {}, + ...overrides, + }) as PageLayoutWidget; + +const makeDynamicRelationWidget = (suffix: string): PageLayoutWidget => + makeWidget({ + id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${suffix}`, + type: WidgetType.VIEW, + title: `Dynamic Relation ${suffix}`, + }); + +const makeServerLayout = (tabs: PageLayout['tabs']): PageLayout => + ({ + id: 'layout-1', + name: 'Layout', + type: 'RECORD_PAGE', + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + tabs, + }) as PageLayout; + +const makeDraftLayout = (tabs: DraftPageLayout['tabs']): DraftPageLayout => + ({ + id: 'layout-1', + name: 'Layout', + type: 'RECORD_PAGE', + tabs, + }) as DraftPageLayout; + +const makeTab = ( + id: string, + widgets: PageLayoutWidget[], +): PageLayout['tabs'][number] => + ({ + id, + title: 'Tab', + position: 0, + pageLayoutId: 'layout-1', + applicationId: 'app-1', + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + widgets, + }) as PageLayout['tabs'][number]; + +describe('reInjectDynamicRelationWidgetsFromDraft', () => { + it('should return server layout unchanged when draft has no dynamic widgets', () => { + const serverLayout = makeServerLayout([ + makeTab('tab-1', [makeWidget({ id: 'w1' })]), + ]); + const draft = makeDraftLayout([ + makeTab('tab-1', [makeWidget({ id: 'w1' })]), + ]); + + const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft); + + expect(result).toBe(serverLayout); + }); + + it('should inject dynamic widgets after the first FIELDS widget', () => { + const fieldsWidget = makeWidget({ + id: 'fields-1', + type: WidgetType.FIELDS, + }); + const timelineWidget = makeWidget({ + id: 'timeline-1', + type: WidgetType.TIMELINE, + }); + const dynamicWidget = makeDynamicRelationWidget('relation-1'); + + const serverLayout = makeServerLayout([ + makeTab('tab-1', [fieldsWidget, timelineWidget]), + ]); + const draft = makeDraftLayout([ + makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]), + ]); + + const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft); + + expect(result.tabs[0].widgets).toHaveLength(3); + expect(result.tabs[0].widgets[0].id).toBe('fields-1'); + expect(result.tabs[0].widgets[1].id).toBe( + `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`, + ); + expect(result.tabs[0].widgets[2].id).toBe('timeline-1'); + }); + + it('should append dynamic widgets if no FIELDS widget exists in the tab', () => { + const timelineWidget = makeWidget({ + id: 'timeline-1', + type: WidgetType.TIMELINE, + }); + const dynamicWidget = makeDynamicRelationWidget('relation-1'); + + const serverLayout = makeServerLayout([makeTab('tab-1', [timelineWidget])]); + const draft = makeDraftLayout([ + makeTab('tab-1', [timelineWidget, dynamicWidget]), + ]); + + const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft); + + expect(result.tabs[0].widgets).toHaveLength(2); + expect(result.tabs[0].widgets[0].id).toBe('timeline-1'); + expect(result.tabs[0].widgets[1].id).toBe( + `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`, + ); + }); + + it('should handle multiple tabs independently', () => { + const fieldsWidget1 = makeWidget({ + id: 'fields-1', + type: WidgetType.FIELDS, + }); + const fieldsWidget2 = makeWidget({ + id: 'fields-2', + type: WidgetType.FIELDS, + }); + const dynamicWidget = makeDynamicRelationWidget('rel-1'); + + const serverLayout = makeServerLayout([ + makeTab('tab-1', [fieldsWidget1]), + makeTab('tab-2', [fieldsWidget2]), + ]); + const draft = makeDraftLayout([ + makeTab('tab-1', [fieldsWidget1, dynamicWidget]), + makeTab('tab-2', [fieldsWidget2]), + ]); + + const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft); + + // Tab 1 should have dynamic widget injected + expect(result.tabs[0].widgets).toHaveLength(2); + // Tab 2 should remain unchanged + expect(result.tabs[1].widgets).toHaveLength(1); + }); + + it('should inject multiple dynamic widgets', () => { + const fieldsWidget = makeWidget({ + id: 'fields-1', + type: WidgetType.FIELDS, + }); + const dynamicWidget1 = makeDynamicRelationWidget('rel-1'); + const dynamicWidget2 = makeDynamicRelationWidget('rel-2'); + + const serverLayout = makeServerLayout([makeTab('tab-1', [fieldsWidget])]); + const draft = makeDraftLayout([ + makeTab('tab-1', [fieldsWidget, dynamicWidget1, dynamicWidget2]), + ]); + + const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft); + + expect(result.tabs[0].widgets).toHaveLength(3); + expect(result.tabs[0].widgets[0].id).toBe('fields-1'); + expect(result.tabs[0].widgets[1].id).toBe( + `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-1`, + ); + expect(result.tabs[0].widgets[2].id).toBe( + `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-2`, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/sortTabsByPosition.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/sortTabsByPosition.test.ts new file mode 100644 index 0000000000..2359ecf51d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/sortTabsByPosition.test.ts @@ -0,0 +1,36 @@ +import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; + +describe('sortTabsByPosition', () => { + it('should sort tabs by position ascending', () => { + const tabs = [ + { id: 'c', position: 3 }, + { id: 'a', position: 1 }, + { id: 'b', position: 2 }, + ]; + + const result = sortTabsByPosition(tabs); + + expect(result.map((t) => t.id)).toEqual(['a', 'b', 'c']); + }); + + it('should return empty array for empty input', () => { + expect(sortTabsByPosition([])).toEqual([]); + }); + + it('should not mutate the original array', () => { + const tabs = [ + { id: 'b', position: 2 }, + { id: 'a', position: 1 }, + ]; + + sortTabsByPosition(tabs); + + expect(tabs[0].id).toBe('b'); + }); + + it('should handle single element', () => { + const tabs = [{ id: 'a', position: 0 }]; + + expect(sortTabsByPosition(tabs)).toEqual([{ id: 'a', position: 0 }]); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/transformPageLayout.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/transformPageLayout.test.ts new file mode 100644 index 0000000000..0c051d2854 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/transformPageLayout.test.ts @@ -0,0 +1,111 @@ +import { transformPageLayout } from '@/page-layout/utils/transformPageLayout'; +import { + type PageLayout, + PageLayoutTabLayoutMode, + PageLayoutType, +} from '~/generated-metadata/graphql'; + +const makePageLayout = (overrides: Partial = {}): PageLayout => + ({ + __typename: 'PageLayout', + id: 'layout-1', + name: 'Test Layout', + type: PageLayoutType.RECORD_PAGE, + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + tabs: [], + ...overrides, + }) as PageLayout; + +describe('transformPageLayout', () => { + it('should default tabs to empty array when null', () => { + const layout = makePageLayout({ tabs: null as any }); + + const result = transformPageLayout(layout); + + expect(result.tabs).toEqual([]); + }); + + it('should sort tabs by position', () => { + const layout = makePageLayout({ + tabs: [ + { + id: 'tab-2', + position: 2, + title: 'Second', + pageLayoutId: 'layout-1', + applicationId: 'app-1', + layoutMode: PageLayoutTabLayoutMode.GRID, + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + }, + { + id: 'tab-1', + position: 1, + title: 'First', + pageLayoutId: 'layout-1', + applicationId: 'app-1', + layoutMode: PageLayoutTabLayoutMode.GRID, + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + }, + ] as any, + }); + + const result = transformPageLayout(layout); + + expect(result.tabs[0].id).toBe('tab-1'); + expect(result.tabs[1].id).toBe('tab-2'); + }); + + it('should default widgets to empty array when null', () => { + const layout = makePageLayout({ + tabs: [ + { + id: 'tab-1', + position: 0, + title: 'Tab', + pageLayoutId: 'layout-1', + applicationId: 'app-1', + layoutMode: PageLayoutTabLayoutMode.GRID, + widgets: null, + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + }, + ] as any, + }); + + const result = transformPageLayout(layout); + + expect(result.tabs[0].widgets).toEqual([]); + }); + + it('should preserve existing widgets', () => { + const widget = { + id: 'w1', + title: 'Widget', + type: 'FIELDS', + }; + + const layout = makePageLayout({ + tabs: [ + { + id: 'tab-1', + position: 0, + title: 'Tab', + pageLayoutId: 'layout-1', + applicationId: 'app-1', + layoutMode: PageLayoutTabLayoutMode.GRID, + widgets: [widget], + createdAt: '2025-01-01', + updatedAt: '2025-01-01', + }, + ] as any, + }); + + const result = transformPageLayout(layout); + + expect(result.tabs[0].widgets).toHaveLength(1); + expect(result.tabs[0].widgets[0].id).toBe('w1'); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetFieldDiff.ts b/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetFieldDiff.ts new file mode 100644 index 0000000000..b04802dbfb --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetFieldDiff.ts @@ -0,0 +1,65 @@ +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { isDefined } from 'twenty-shared/utils'; + +export const computeFieldsWidgetFieldDiff = ( + persistedGroups: FieldsWidgetGroup[], + draftGroups: FieldsWidgetGroup[], +) => { + const fieldUpdates: Array<{ + viewFieldId: string; + isVisible?: boolean; + position?: number; + viewFieldGroupId?: string; + }> = []; + + const persistedFieldMap = new Map< + string, + { + isVisible: boolean; + position: number; + groupId: string; + } + >(); + + for (const group of persistedGroups) { + for (const field of group.fields) { + if (isDefined(field.viewFieldId)) { + persistedFieldMap.set(field.viewFieldId, { + isVisible: field.isVisible, + position: field.position, + groupId: group.id, + }); + } + } + } + + for (const draftGroup of draftGroups) { + for (const draftField of draftGroup.fields) { + if (!isDefined(draftField.viewFieldId)) { + continue; + } + + const persistedField = persistedFieldMap.get(draftField.viewFieldId); + + if (!isDefined(persistedField)) { + continue; + } + + const hasVisibilityChange = + persistedField.isVisible !== draftField.isVisible; + const hasPositionChange = persistedField.position !== draftField.position; + const hasGroupChange = persistedField.groupId !== draftGroup.id; + + if (hasVisibilityChange || hasPositionChange || hasGroupChange) { + fieldUpdates.push({ + viewFieldId: draftField.viewFieldId, + ...(hasVisibilityChange ? { isVisible: draftField.isVisible } : {}), + ...(hasPositionChange ? { position: draftField.position } : {}), + ...(hasGroupChange ? { viewFieldGroupId: draftGroup.id } : {}), + }); + } + } + } + + return fieldUpdates; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetGroupDiff.ts b/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetGroupDiff.ts new file mode 100644 index 0000000000..97468fcde0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/computeFieldsWidgetGroupDiff.ts @@ -0,0 +1,34 @@ +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { isDefined } from 'twenty-shared/utils'; + +export const computeFieldsWidgetGroupDiff = ( + persistedGroups: FieldsWidgetGroup[], + draftGroups: FieldsWidgetGroup[], +) => { + const persistedGroupIds = new Set(persistedGroups.map((g) => g.id)); + const draftGroupIds = new Set(draftGroups.map((g) => g.id)); + + const createdGroups = draftGroups.filter((g) => !persistedGroupIds.has(g.id)); + + const deletedGroups = persistedGroups.filter((g) => !draftGroupIds.has(g.id)); + + const updatedGroups = draftGroups.filter((draftGroup) => { + if (!persistedGroupIds.has(draftGroup.id)) { + return false; + } + + const persistedGroup = persistedGroups.find((g) => g.id === draftGroup.id); + + if (!isDefined(persistedGroup)) { + return false; + } + + return ( + persistedGroup.name !== draftGroup.name || + persistedGroup.position !== draftGroup.position || + persistedGroup.isVisible !== draftGroup.isVisible + ); + }); + + return { createdGroups, deletedGroups, updatedGroups }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts index fa0ba5581b..694f89ea62 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts @@ -1,4 +1,5 @@ import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; +import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget'; import { type UpdatePageLayoutWithTabsInput } from '~/generated-metadata/graphql'; export const convertPageLayoutDraftToUpdateInput = ( @@ -12,20 +13,22 @@ export const convertPageLayoutDraftToUpdateInput = ( id: tab.id, title: tab.title, position: tab.position, - widgets: tab.widgets.map((widget) => ({ - id: widget.id, - pageLayoutTabId: widget.pageLayoutTabId, - title: widget.title, - type: widget.type, - objectMetadataId: widget.objectMetadataId ?? null, - gridPosition: { - row: widget.gridPosition.row, - column: widget.gridPosition.column, - rowSpan: widget.gridPosition.rowSpan, - columnSpan: widget.gridPosition.columnSpan, - }, - configuration: widget.configuration ?? null, - })), + widgets: tab.widgets + .filter((widget) => !isDynamicRelationWidget(widget)) + .map((widget) => ({ + id: widget.id, + pageLayoutTabId: widget.pageLayoutTabId, + title: widget.title, + type: widget.type, + objectMetadataId: widget.objectMetadataId ?? null, + gridPosition: { + row: widget.gridPosition.row, + column: widget.gridPosition.column, + rowSpan: widget.gridPosition.rowSpan, + columnSpan: widget.gridPosition.columnSpan, + }, + configuration: widget.configuration ?? null, + })), })), }; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDefaultRecordPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/utils/getDefaultRecordPageLayoutId.ts new file mode 100644 index 0000000000..ad98e70733 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultRecordPageLayoutId.ts @@ -0,0 +1,34 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId'; +import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId'; +import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId'; +import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId'; +import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId'; +import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId'; +import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId'; +import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId'; +import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId'; + +const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record = { + [CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Opportunity]: + DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.WorkflowVersion]: + DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID, +}; + +export const getDefaultRecordPageLayoutId = ({ + targetObjectNameSingular, +}: { + targetObjectNameSingular: string; +}): string => { + return ( + OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ?? + DEFAULT_RECORD_PAGE_LAYOUT_ID + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts index 301534a163..c0b0202fdd 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts @@ -1,6 +1,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type PageLayout } from '@/page-layout/types/PageLayout'; 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 { WidgetConfigurationType, @@ -12,7 +13,7 @@ const getRelationFieldWidgetToInsert = ( tabId: string, ): PageLayoutWidget => ({ __typename: 'PageLayoutWidget' as const, - id: `dynamic-relation-widget-${field.id}-${field.label}`, + id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${field.id}-${field.label}`, pageLayoutTabId: tabId, title: field.label, type: WidgetType.FIELD, diff --git a/packages/twenty-front/src/modules/page-layout/utils/isDynamicRelationWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/isDynamicRelationWidget.ts new file mode 100644 index 0000000000..4ed4a6fc10 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/isDynamicRelationWidget.ts @@ -0,0 +1,7 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; + +export const DYNAMIC_RELATION_WIDGET_ID_PREFIX = 'dynamic-relation-widget-'; + +export const isDynamicRelationWidget = (widget: PageLayoutWidget): boolean => { + return widget.id.startsWith(DYNAMIC_RELATION_WIDGET_ID_PREFIX); +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft.ts b/packages/twenty-front/src/modules/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft.ts new file mode 100644 index 0000000000..514e272770 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft.ts @@ -0,0 +1,54 @@ +import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; +import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget'; +import { WidgetType } from '~/generated-metadata/graphql'; + +export const reInjectDynamicRelationWidgetsFromDraft = ( + serverLayout: PageLayout, + previousDraft: DraftPageLayout, +): PageLayout => { + const dynamicWidgetsByTabId = new Map( + previousDraft.tabs.map((tab) => [ + tab.id, + tab.widgets.filter(isDynamicRelationWidget), + ]), + ); + + const hasDynamicWidgets = [...dynamicWidgetsByTabId.values()].some( + (widgets) => widgets.length > 0, + ); + + if (!hasDynamicWidgets) { + return serverLayout; + } + + return { + ...serverLayout, + tabs: serverLayout.tabs.map((tab) => { + const relationWidgets = dynamicWidgetsByTabId.get(tab.id); + + if (!relationWidgets || relationWidgets.length === 0) { + return tab; + } + + const firstFieldsWidgetIndex = tab.widgets.findIndex( + (widget) => widget.type === WidgetType.FIELDS, + ); + + if (firstFieldsWidgetIndex === -1) { + return { + ...tab, + widgets: [...tab.widgets, ...relationWidgets], + }; + } + + const widgetsBefore = tab.widgets.slice(0, firstFieldsWidgetIndex + 1); + const widgetsAfter = tab.widgets.slice(firstFieldsWidgetIndex + 1); + + return { + ...tab, + widgets: [...widgetsBefore, ...relationWidgets, ...widgetsAfter], + }; + }), + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts index 811044b47a..7edff183e4 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts @@ -10,11 +10,8 @@ export const transformPageLayout = ( tabs: (pageLayout.tabs ?? []) .toSorted((a, b) => a.position - b.position) .map((tab): PageLayoutTab => { - // TODO: remove this once the frontend consumes the new type - const { layoutMode: _layoutMode, ...tabWithoutLayoutMode } = tab; - return { - ...tabWithoutLayoutMode, + ...tab, widgets: tab.widgets ?? [], }; }), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/assertFieldWidgetOrThrow.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/assertFieldWidgetOrThrow.test.ts new file mode 100644 index 0000000000..d3bbdf1e38 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/assertFieldWidgetOrThrow.test.ts @@ -0,0 +1,61 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { assertFieldWidgetOrThrow } from '@/page-layout/widgets/field/utils/assertFieldWidgetOrThrow'; +import { + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +describe('assertFieldWidgetOrThrow', () => { + it('should not throw for a valid field widget', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: { + __typename: 'FieldConfiguration', + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId: 'fm-1', + layout: 'FIELD', + }, + } as PageLayoutWidget; + + expect(() => assertFieldWidgetOrThrow(widget)).not.toThrow(); + }); + + it('should throw when configuration is undefined', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: undefined, + } as unknown as PageLayoutWidget; + + expect(() => assertFieldWidgetOrThrow(widget)).toThrow( + 'Widget configuration is required', + ); + }); + + it('should throw when configuration typename does not match', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: { + __typename: 'WidgetConfiguration', + }, + } as unknown as PageLayoutWidget; + + expect(() => assertFieldWidgetOrThrow(widget)).toThrow( + 'Expected FieldConfiguration but got WidgetConfiguration', + ); + }); + + it('should throw when configuration typename is empty string', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: { + __typename: '', + }, + } as unknown as PageLayoutWidget; + + expect(() => assertFieldWidgetOrThrow(widget)).toThrow(); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/generateFieldWidgetInstanceId.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/generateFieldWidgetInstanceId.test.ts new file mode 100644 index 0000000000..a86fe90a77 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/generateFieldWidgetInstanceId.test.ts @@ -0,0 +1,25 @@ +import { generateFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils/generateFieldWidgetInstanceId'; + +describe('generateFieldWidgetInstanceId', () => { + it('should generate an instance id without right drawer suffix', () => { + const result = generateFieldWidgetInstanceId({ + widgetId: 'widget-1', + recordId: 'record-1', + fieldName: 'name', + isInRightDrawer: false, + }); + + expect(result).toBe('widget-1-field-widget-record-1-name'); + }); + + it('should append right drawer suffix when isInRightDrawer is true', () => { + const result = generateFieldWidgetInstanceId({ + widgetId: 'widget-1', + recordId: 'record-1', + fieldName: 'name', + isInRightDrawer: true, + }); + + expect(result).toBe('widget-1-field-widget-record-1-name-right-drawer'); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/isFieldWidget.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/isFieldWidget.test.ts new file mode 100644 index 0000000000..01d31cabe9 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/__tests__/isFieldWidget.test.ts @@ -0,0 +1,60 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget'; +import { + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +describe('isFieldWidget', () => { + it('should return true for a FIELD widget with FieldConfiguration', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: { + __typename: 'FieldConfiguration', + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId: 'fm-1', + layout: 'FIELD', + }, + } as PageLayoutWidget; + + expect(isFieldWidget(widget)).toBe(true); + }); + + it('should return false for a non-FIELD widget type', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELDS, + configuration: { + __typename: 'FieldConfiguration', + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId: 'fm-1', + layout: 'FIELD', + }, + } as PageLayoutWidget; + + expect(isFieldWidget(widget)).toBe(false); + }); + + it('should return false when configuration is null', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: null, + } as unknown as PageLayoutWidget; + + expect(isFieldWidget(widget)).toBe(false); + }); + + it('should return false when configuration typename does not match', () => { + const widget = { + id: 'w1', + type: WidgetType.FIELD, + configuration: { + __typename: 'WidgetConfiguration', + }, + } as unknown as PageLayoutWidget; + + expect(isFieldWidget(widget)).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx index 81b4d2d64e..eafbe4e604 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx @@ -7,43 +7,56 @@ import { } from '@hello-pangea/dnd'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { - type FieldsConfiguration, - type FieldsConfigurationFieldItem, - type FieldsConfigurationSection, -} from '@/page-layout/types/FieldsConfiguration'; -import { FieldsConfigurationSectionEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor'; +import { FieldsConfigurationGroupEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor'; +import { useCreateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup'; +import { useFieldsWidgetGroupsDraft } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft'; +import { useMoveFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveFieldInDraft'; +import { useReorderFieldsWidgetEditorGroups } from '@/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups'; +import { useToggleFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft'; +import { useLingui } from '@lingui/react/macro'; -const StyledSectionsDroppable = styled.div` +const StyledGroupsDroppable = styled.div` display: flex; flex-direction: column; width: 100%; `; type FieldsConfigurationEditorProps = { - configuration: FieldsConfiguration; - onChange: (configuration: FieldsConfiguration) => void; + pageLayoutId: string; + widgetId: string; }; export const FieldsConfigurationEditor = ({ - configuration, - onChange, + pageLayoutId, + widgetId, }: FieldsConfigurationEditorProps) => { + const { t } = useLingui(); const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - const handleSectionChange = ( - sectionId: string, - updatedSection: FieldsConfigurationSection, - ) => { - const updatedSections = configuration.sections.map((section) => - section.id === sectionId ? updatedSection : section, - ); + const { draftGroups } = useFieldsWidgetGroupsDraft({ + pageLayoutId, + widgetId, + }); - onChange({ - ...configuration, - sections: updatedSections, - }); - }; + const { createGroup } = useCreateFieldsWidgetEditorGroup({ + pageLayoutId, + widgetId, + }); + + const { reorderGroups } = useReorderFieldsWidgetEditorGroups({ + pageLayoutId, + widgetId, + }); + + const { moveField } = useMoveFieldInDraft({ + pageLayoutId, + widgetId, + }); + + const { toggleFieldVisibility } = useToggleFieldVisibilityInDraft({ + pageLayoutId, + widgetId, + }); const handleDragEnd = (result: DropResult) => { const { source, destination, type } = result; @@ -59,8 +72,8 @@ export const FieldsConfigurationEditor = ({ return; } - if (type === 'SECTION') { - handleSectionReorder(source.index, destination.index); + if (type === 'GROUP') { + handleGroupReorder(source.index, destination.index); } else if (type === 'FIELD') { handleFieldMove( source.droppableId, @@ -71,144 +84,81 @@ export const FieldsConfigurationEditor = ({ } }; - const handleSectionReorder = ( + const handleGroupReorder = ( sourceIndex: number, destinationIndex: number, ) => { - const sortedSections = [...configuration.sections].sort( + const sortedGroups = [...draftGroups].sort( (a, b) => a.position - b.position, ); - const [movedSection] = sortedSections.splice(sourceIndex, 1); - sortedSections.splice(destinationIndex, 0, movedSection); + const reorderedGroupIds = sortedGroups.map((g) => g.id); + const [movedGroupId] = reorderedGroupIds.splice(sourceIndex, 1); + reorderedGroupIds.splice(destinationIndex, 0, movedGroupId); - const updatedSections = sortedSections.map((section, index) => ({ - ...section, - position: index, - })); - - onChange({ - ...configuration, - sections: updatedSections, - }); + reorderGroups(reorderedGroupIds); }; const handleFieldMove = ( - sourceSectionId: string, - destinationSectionId: string, + sourceGroupId: string, + destinationGroupId: string, sourceIndex: number, destinationIndex: number, ) => { - const sourceSection = configuration.sections.find( - (s) => `section-${s.id}` === sourceSectionId, + const cleanSourceGroupId = sourceGroupId.replace('group-', ''); + const cleanDestinationGroupId = destinationGroupId.replace('group-', ''); + + moveField( + cleanSourceGroupId, + cleanDestinationGroupId, + sourceIndex, + destinationIndex, ); - const destinationSection = configuration.sections.find( - (s) => `section-${s.id}` === destinationSectionId, - ); - - if (!sourceSection || !destinationSection) { - return; - } - - const sourceSortedFields = [...sourceSection.fields].sort( - (a, b) => a.position - b.position, - ); - - if (sourceSectionId === destinationSectionId) { - // Reorder within the same section - const [movedField] = sourceSortedFields.splice(sourceIndex, 1); - sourceSortedFields.splice(destinationIndex, 0, movedField); - - const updatedFields = sourceSortedFields.map((field, index) => ({ - ...field, - position: index, - })); - - handleSectionChange(sourceSection.id, { - ...sourceSection, - fields: updatedFields, - }); - } else { - // Move field between sections - const [movedField] = sourceSortedFields.splice(sourceIndex, 1); - - const destinationSortedFields = [...destinationSection.fields].sort( - (a, b) => a.position - b.position, - ); - destinationSortedFields.splice(destinationIndex, 0, movedField); - - const updatedSourceFields = sourceSortedFields.map( - (field, index) => - ({ - ...field, - position: index, - }) satisfies FieldsConfigurationFieldItem, - ); - - const updatedDestinationFields = destinationSortedFields.map( - (field, index) => - ({ - ...field, - position: index, - }) satisfies FieldsConfigurationFieldItem, - ); - - const updatedSections = configuration.sections.map((section) => { - if (section.id === sourceSection.id) { - return { ...section, fields: updatedSourceFields }; - } - if (section.id === destinationSection.id) { - return { ...section, fields: updatedDestinationFields }; - } - return section; - }); - - onChange({ - ...configuration, - sections: updatedSections, - }); - } }; - const sortedSections = [...configuration.sections].sort( - (a, b) => a.position - b.position, - ); + const handleAddGroup = () => { + const newGroupName = t`New Group`; + createGroup(newGroupName); + }; - if (sortedSections.length === 0) { + const sortedGroups = [...draftGroups].sort((a, b) => a.position - b.position); + + if (sortedGroups.length === 0) { return null; } return ( - + {(provided) => ( - - {sortedSections.map((section, index) => ( + {sortedGroups.map((group, index) => ( {(draggableProvided, snapshot) => ( - - handleSectionChange(section.id, updatedSection) - } draggableProvided={draggableProvided} isDragging={snapshot.isDragging} + onAddGroup={handleAddGroup} + onToggleFieldVisibility={(fieldMetadataId) => + toggleFieldVisibility(group.id, fieldMetadataId) + } /> )} ))} {provided.placeholder} - + )} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx index cb6abdae07..0a560ab049 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx @@ -15,7 +15,7 @@ export const FieldsConfigurationFieldEditor = ({ onToggleVisibility, }: FieldsConfigurationFieldEditorProps) => { const { getIcon } = useIcons(); - const isVisible = field.conditionalDisplay !== false; + const isVisible = field.isVisible ?? true; const FieldIcon = getIcon(fieldMetadata.icon); return ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx similarity index 55% rename from packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx rename to packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx index 9de7c994a7..da4f2368d8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx @@ -5,11 +5,8 @@ import { useLingui } from '@lingui/react/macro'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { - type FieldsConfigurationFieldItem, - type FieldsConfigurationSection, -} from '@/page-layout/types/FieldsConfiguration'; import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; import { IconDotsVertical, IconNewSection } from 'twenty-ui/display'; import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation'; @@ -18,7 +15,7 @@ const StyledFieldsDroppable = styled.div` flex-direction: column; `; -const StyledSectionContainer = styled.div<{ isDragging: boolean }>` +const StyledGroupContainer = styled.div<{ isDragging: boolean }>` background: ${({ isDragging, theme }) => isDragging ? theme.background.primary : 'transparent'}; border: 1px solid @@ -30,61 +27,31 @@ const StyledSectionContainer = styled.div<{ isDragging: boolean }>` width: 100%; `; -type FieldsConfigurationSectionEditorProps = { - section: FieldsConfigurationSection; +type FieldsConfigurationGroupEditorProps = { + group: FieldsWidgetGroup; index: number; objectMetadataItem: ObjectMetadataItem; - onSectionChange: (section: FieldsConfigurationSection) => void; draggableProvided: DraggableProvided; isDragging: boolean; + onAddGroup?: () => void; + onToggleFieldVisibility: (fieldMetadataId: string) => void; }; -export const FieldsConfigurationSectionEditor = ({ - section, - objectMetadataItem, - onSectionChange, +export const FieldsConfigurationGroupEditor = ({ + group, draggableProvided, isDragging, -}: FieldsConfigurationSectionEditorProps) => { + onAddGroup, + onToggleFieldVisibility, +}: FieldsConfigurationGroupEditorProps) => { const { t } = useLingui(); - const handleFieldChange = ( - fieldMetadataId: string, - updatedField: FieldsConfigurationFieldItem, - ) => { - const updatedFields = section.fields.map((field) => - field.fieldMetadataId === fieldMetadataId ? updatedField : field, - ); - - onSectionChange({ - ...section, - fields: updatedFields, - }); - }; - - const handleToggleFieldVisibility = (fieldMetadataId: string) => { - const field = section.fields.find( - (f) => f.fieldMetadataId === fieldMetadataId, - ); - - if (!field) { - return; - } - - const updatedField: FieldsConfigurationFieldItem = { - ...field, - conditionalDisplay: false, - }; - - handleFieldChange(fieldMetadataId, updatedField); - }; - - const sortedFields = [...section.fields].sort( + const sortedFields = [...group.fields].sort( (a, b) => a.position - b.position, ); return ( - { e.stopPropagation(); - // TODO: Add section menu + // TODO: Add group menu }, }, ]} /> - + {(droppableProvided) => ( {sortedFields.map((field, fieldIndex) => { - const fieldMetadata = objectMetadataItem.fields.find( - (f) => f.id === field.fieldMetadataId, - ); - - if (!fieldMetadata) { - return null; - } - return ( - handleToggleFieldVisibility(field.fieldMetadataId) - } + field={{ + fieldMetadataId: field.fieldMetadataItem.id, + position: field.position, + isVisible: field.isVisible, + }} + fieldMetadata={field.fieldMetadataItem} + onToggleVisibility={() => { + onToggleFieldVisibility(field.fieldMetadataItem.id); + }} /> } /> @@ -151,11 +114,9 @@ export const FieldsConfigurationSectionEditor = ({ { - // TODO: Implement add section - }} + text={t`Add a Group`} + onClick={onAddGroup} /> - + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx index c93db53823..03681f1a21 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx @@ -21,8 +21,8 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { FieldsWidgetCellEditModePortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal'; import { FieldsWidgetCellHoveredPortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal'; -import { FieldsWidgetSectionContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetSectionContainer'; -import { useFieldsWidgetSectionsWithIndices } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices'; +import { FieldsWidgetGroupContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupContainer'; +import { useFieldsWidgetGroupsForDisplay } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay'; import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; @@ -38,6 +38,7 @@ import { AnimatedPlaceholderEmptyTitle, EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from 'twenty-ui/layout'; +import { type FieldsConfiguration } from '~/generated-metadata/graphql'; const StyledContainer = styled.div` box-sizing: border-box; @@ -64,7 +65,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => { const targetRecord = useTargetRecord(); const { isInRightDrawer } = useLayoutRenderingContext(); - const instanceId = `${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`; + const instanceId = `fields-${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`; const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({ objectRecordId: targetRecord.id, @@ -91,11 +92,19 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => { instanceId, ); - const { sectionsWithFieldIndices } = useFieldsWidgetSectionsWithIndices( - targetRecord.targetObjectNameSingular, + const fieldsConfiguration = widget.configuration as FieldsConfiguration; + + const { groups } = useFieldsWidgetGroupsForDisplay({ + widgetId: widget.id, + viewId: fieldsConfiguration.viewId ?? null, + objectNameSingular: targetRecord.targetObjectNameSingular, + }); + + const flattenedFieldMetadataItems = groups.flatMap((group) => + group.fields.map((field) => field.fieldMetadataItem), ); - const hasFieldsToDisplay = sectionsWithFieldIndices.length > 0; + const hasFieldsToDisplay = groups.length > 0; if (!hasFieldsToDisplay) { return ( @@ -128,118 +137,113 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => { instanceId, }} > - {sectionsWithFieldIndices.map((section) => ( - + {groups.map((group) => ( + {isPrefetchLoading ? ( ) : ( <> - {section.fields.map( - ({ field: fieldMetadataItem, globalIndex }) => { - const isActivityTarget = isActivityTargetField( - fieldMetadataItem.name, - targetRecord.targetObjectNameSingular, - ); + {group.fields.map(({ fieldMetadataItem, globalIndex }) => { + const isActivityTarget = isActivityTargetField( + fieldMetadataItem.name, + targetRecord.targetObjectNameSingular, + ); - return ( - + setRecordFieldListHoverPosition(globalIndex), + anchorId: `${getRecordFieldInputInstanceId({ + recordId: targetRecord.id, + fieldName: fieldMetadataItem.name, + prefix: instanceId, + })}`, + isForbidden: isJunctionRelationForbidden({ + fieldMetadataItem, + sourceObjectMetadataId: objectMetadataItem.id, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }), + }} + > + {isActivityTarget ? ( + - setRecordFieldListHoverPosition(globalIndex), - anchorId: `${getRecordFieldInputInstanceId({ - recordId: targetRecord.id, - fieldName: fieldMetadataItem.name, - prefix: instanceId, - })}`, - isForbidden: isJunctionRelationForbidden({ - fieldMetadataItem, - sourceObjectMetadataId: objectMetadataItem.id, - objectMetadataItems, - objectPermissionsByObjectMetadataId, - }), - }} - > - {isActivityTarget ? ( - - ) : ( - - - - )} - - ); - }, - )} + )} + activityObjectNameSingular={ + targetRecord.targetObjectNameSingular as + | CoreObjectNameSingular.Note + | CoreObjectNameSingular.Task + } + activityRecordId={targetRecord.id} + showLabel={true} + maxWidth={200} + /> + ) : ( + + + + )} + + ); + })} )} - + ))} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx index 7f5a2b65b4..db1cb929ab 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx @@ -1,3 +1,4 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider'; import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; @@ -5,7 +6,6 @@ import { recordFieldListCellEditModePositionComponentState } from '@/object-reco import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput'; import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal'; import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode'; -import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isDefined } from 'twenty-shared/utils'; @@ -13,11 +13,13 @@ import { isDefined } from 'twenty-shared/utils'; type FieldsWidgetCellEditModePortalProps = { objectMetadataItem: ObjectMetadataItem; recordId: string; + flattenedFieldMetadataItems: FieldMetadataItem[]; }; export const FieldsWidgetCellEditModePortal = ({ objectMetadataItem, recordId, + flattenedFieldMetadataItems, }: FieldsWidgetCellEditModePortalProps) => { const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldListComponentInstanceContext, @@ -27,10 +29,6 @@ export const FieldsWidgetCellEditModePortal = ({ recordFieldListCellEditModePositionComponentState, ); - const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields( - objectMetadataItem.nameSingular, - ); - const editedFieldMetadataItem = isDefined(editModePosition) ? flattenedFieldMetadataItems.at(editModePosition) : undefined; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx index 1ccbc66241..633939e7f2 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx @@ -1,10 +1,10 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordFieldListCellHoveredPortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent'; import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider'; import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState'; import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal'; -import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isDefined } from 'twenty-shared/utils'; @@ -12,11 +12,13 @@ import { isDefined } from 'twenty-shared/utils'; type FieldsWidgetCellHoveredPortalProps = { objectMetadataItem: ObjectMetadataItem; recordId: string; + flattenedFieldMetadataItems: FieldMetadataItem[]; }; export const FieldsWidgetCellHoveredPortal = ({ objectMetadataItem, recordId, + flattenedFieldMetadataItems, }: FieldsWidgetCellHoveredPortalProps) => { const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldListComponentInstanceContext, @@ -26,10 +28,6 @@ export const FieldsWidgetCellHoveredPortal = ({ recordFieldListHoverPositionComponentState, ); - const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields( - objectMetadataItem.nameSingular, - ); - const hoveredFieldMetadataItem = isDefined(hoverPosition) ? flattenedFieldMetadataItems.at(hoverPosition) : undefined; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetSectionContainer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetGroupContainer.tsx similarity index 88% rename from packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetSectionContainer.tsx rename to packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetGroupContainer.tsx index 66bf6038f4..437cc9736c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetSectionContainer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetGroupContainer.tsx @@ -1,5 +1,5 @@ -import { useTheme } from '@emotion/react'; import isPropValid from '@emotion/is-prop-valid'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useState } from 'react'; import { IconChevronDown } from 'twenty-ui/display'; @@ -28,24 +28,24 @@ const StyledChevronIcon = styled(IconChevronDown, { `transform ${theme.animation.duration.normal}s ease`}; `; -type FieldsWidgetSectionContainerProps = { +type FieldsWidgetGroupContainerProps = { children: React.ReactNode; title: string; }; -export const FieldsWidgetSectionContainer = ({ +export const FieldsWidgetGroupContainer = ({ children, title, -}: FieldsWidgetSectionContainerProps) => { +}: FieldsWidgetGroupContainerProps) => { const [isExpanded, setIsExpanded] = useState(true); const theme = useTheme(); - const handleToggleSection = () => + const handleToggleGroup = () => setIsExpanded((previousIsExpanded) => !previousIsExpanded); return (
- + {title} { + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const { groups: serverGroups } = useFieldsWidgetEditorGroupsData({ + viewId, + objectNameSingular: objectMetadataItem.nameSingular, + }); + + useInitializeFieldsWidgetGroupsDraft({ + pageLayoutId, + widgetId, + serverGroups, + }); + + return null; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup.ts new file mode 100644 index 0000000000..147159377e --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup.ts @@ -0,0 +1,60 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { v4 } from 'uuid'; + +type UseCreateFieldsWidgetEditorGroupParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useCreateFieldsWidgetEditorGroup = ({ + pageLayoutId, + widgetId, +}: UseCreateFieldsWidgetEditorGroupParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const createGroup = useRecoilCallback( + ({ set, snapshot }) => + (name: string) => { + const allDraftGroups = snapshot + .getLoadable(fieldsWidgetGroupsDraftState) + .getValue(); + + const currentGroups = allDraftGroups[widgetId] ?? []; + const maxPosition = Math.max( + ...currentGroups.map((g) => g.position), + -1, + ); + const newId = v4(); + + set(fieldsWidgetGroupsDraftState, (prev) => ({ + ...prev, + [widgetId]: [ + ...(prev[widgetId] ?? []), + { + id: newId, + name, + position: maxPosition + 1, + isVisible: true, + fields: [], + }, + ], + })); + + return newId; + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const createGroupCallback = useCallback( + (name: string) => createGroup(name), + [createGroup], + ); + + return { createGroup: createGroupCallback }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup.ts new file mode 100644 index 0000000000..65d730e1ae --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup.ts @@ -0,0 +1,39 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; + +type UseDeleteFieldsWidgetEditorGroupParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useDeleteFieldsWidgetEditorGroup = ({ + pageLayoutId, + widgetId, +}: UseDeleteFieldsWidgetEditorGroupParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const deleteGroupRecoilCallback = useRecoilCallback( + ({ set }) => + (groupId: string) => { + set(fieldsWidgetGroupsDraftState, (prev) => ({ + ...prev, + [widgetId]: (prev[widgetId] ?? []).filter( + (group) => group.id !== groupId, + ), + })); + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const deleteGroup = useCallback( + (groupId: string) => deleteGroupRecoilCallback(groupId), + [deleteGroupRecoilCallback], + ); + + return { deleteGroup }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts new file mode 100644 index 0000000000..e593372140 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts @@ -0,0 +1,118 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { useGetViewById } from '@/views/hooks/useGetViewById'; +import { useLingui } from '@lingui/react/macro'; +import { useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type UseFieldsWidgetEditorGroupsDataParams = { + viewId: string | null; + objectNameSingular: string; +}; + +export const useFieldsWidgetEditorGroupsData = ({ + viewId, + objectNameSingular, +}: UseFieldsWidgetEditorGroupsDataParams) => { + const { t } = useLingui(); + const { view } = useGetViewById(viewId); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const groups = useMemo(() => { + if (!isDefined(objectMetadataItem)) { + return []; + } + + if (isDefined(view) && isDefined(view.viewFieldGroups)) { + const viewFieldGroups = view.viewFieldGroups; + + const sortedGroups = [...viewFieldGroups].sort( + (a, b) => a.position - b.position, + ); + + let globalIndex = 0; + + return sortedGroups.map((group) => { + const groupFields = [...(group.viewFields ?? [])].sort( + (a, b) => a.position - b.position, + ); + + const fields = groupFields + .map((viewField) => { + const fieldMetadataItem = objectMetadataItem.fields.find( + (f) => f.id === viewField.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return null; + } + + return { + fieldMetadataItem, + position: viewField.position, + isVisible: viewField.isVisible, + globalIndex: globalIndex++, + viewFieldId: viewField.id, + }; + }) + .filter(isDefined); + + return { + id: group.id, + name: group.name, + position: group.position, + isVisible: group.isVisible, + fields, + }; + }); + } + + if (isDefined(view) && view.viewFields.length > 0) { + let globalIndex = 0; + + const fields = [...view.viewFields] + .sort((a, b) => a.position - b.position) + .map((viewField) => { + const fieldMetadataItem = objectMetadataItem.fields.find( + (f) => f.id === viewField.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return null; + } + + return { + fieldMetadataItem, + position: viewField.position, + isVisible: viewField.isVisible, + globalIndex: globalIndex++, + viewFieldId: viewField.id, + }; + }) + .filter(isDefined); + + if (fields.length > 0) { + return [ + { + id: `${view.id}-group-general`, + name: t`General`, + position: 0, + isVisible: true, + fields, + }, + ]; + } + } + + return []; + }, [objectMetadataItem, t, view]); + + return { + groups, + isFromView: + isDefined(view) && + (isDefined(view.viewFieldGroups) || view.viewFields.length > 0), + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts deleted file mode 100644 index 76475773b5..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields'; - -export const useFieldsWidgetFlattenedFields = (objectNameSingular: string) => { - const { sectionsWithFields } = - useFieldsWidgetSectionsWithFields(objectNameSingular); - - const flattenedFieldMetadataItems = sectionsWithFields.flatMap( - (section) => section.fields, - ); - - return { flattenedFieldMetadataItems }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts new file mode 100644 index 0000000000..b40f3ae51c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts @@ -0,0 +1,201 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems'; +import { + type FieldsWidgetGroup, + type FieldsWidgetGroupField, +} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { useGetViewById } from '@/views/hooks/useGetViewById'; +import { useLingui } from '@lingui/react/macro'; +import { useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type UseFieldsWidgetGroupsParams = { + viewId: string | null; + objectNameSingular: string; +}; + +export const useFieldsWidgetGroups = ({ + viewId, + objectNameSingular, +}: UseFieldsWidgetGroupsParams) => { + const { t } = useLingui(); + const { view } = useGetViewById(viewId); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { inlineFieldMetadataItems } = useFieldsWidgetFieldMetadataItems({ + objectNameSingular, + }); + + const groups = useMemo(() => { + if (!isDefined(objectMetadataItem)) { + return []; + } + + // Views are seeded for object + if (isDefined(view) && isDefined(view.viewFieldGroups)) { + const sortedGroups = view.viewFieldGroups.toSorted( + (a, b) => a.position - b.position, + ); + + let globalIndex = 0; + + return sortedGroups + .filter((group) => group.isVisible) + .map((group) => { + const groupFields = [...(group.viewFields ?? [])].sort( + (a, b) => a.position - b.position, + ); + + const fields: FieldsWidgetGroupField[] = groupFields + .filter((field) => field.isVisible) + .map((viewField) => { + const fieldMetadataItem = objectMetadataItem.fields.find( + (f) => f.id === viewField.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return null; + } + + const field: FieldsWidgetGroupField = { + fieldMetadataItem, + position: viewField.position, + isVisible: viewField.isVisible, + globalIndex: globalIndex++, + }; + + return field; + }) + .filter(isDefined); + + return { + id: group.id, + name: group.name, + position: group.position, + isVisible: group.isVisible, + fields, + }; + }) + .filter((group) => group.fields.length > 0); + } + + if (isDefined(view) && view.viewFields.length > 0) { + let globalIndex = 0; + + const fields: FieldsWidgetGroupField[] = [...view.viewFields] + .sort((a, b) => a.position - b.position) + .filter((viewField) => viewField.isVisible) + .map((viewField) => { + const fieldMetadataItem = objectMetadataItem.fields.find( + (f) => f.id === viewField.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return null; + } + + return { + fieldMetadataItem, + position: viewField.position, + isVisible: viewField.isVisible, + globalIndex: globalIndex++, + }; + }) + .filter(isDefined); + + if (fields.length > 0) { + return [ + { + id: `${view.id}-group-general`, + name: t`General`, + position: 0, + isVisible: true, + fields, + }, + ]; + } + } + + const fieldsToDisplay = inlineFieldMetadataItems; + + if (fieldsToDisplay.length === 0) { + return []; + } + + const generalFields: Array<{ + fieldMetadataItem: (typeof fieldsToDisplay)[0]; + position: number; + }> = []; + const otherFields: Array<{ + fieldMetadataItem: (typeof fieldsToDisplay)[0]; + position: number; + }> = []; + + let generalPosition = 0; + let otherPosition = 0; + + fieldsToDisplay.forEach((field) => { + if (field.isCustom === true) { + otherFields.push({ + fieldMetadataItem: field, + position: otherPosition++, + }); + } else { + generalFields.push({ + fieldMetadataItem: field, + position: generalPosition++, + }); + } + }); + + const groups: FieldsWidgetGroup[] = []; + let globalIndex = 0; + + if (generalFields.length > 0) { + groups.push({ + id: `${objectNameSingular}-group-general`, + name: t`General`, + position: 0, + isVisible: true, + fields: generalFields.map((field) => ({ + fieldMetadataItem: field.fieldMetadataItem, + position: field.position, + isVisible: true, + globalIndex: globalIndex++, + })), + }); + } + + if (otherFields.length > 0) { + groups.push({ + id: `${objectNameSingular}-group-other`, + name: t`Other`, + position: 1, + isVisible: true, + fields: otherFields.map((field) => ({ + fieldMetadataItem: field.fieldMetadataItem, + position: field.position, + isVisible: true, + globalIndex: globalIndex++, + })), + }); + } + + return groups; + }, [ + objectMetadataItem, + objectNameSingular, + t, + view, + inlineFieldMetadataItems, + ]); + + return { + groups, + isFromView: + isDefined(view) && + (isDefined(view.viewFieldGroups) || view.viewFields.length > 0), + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft.ts new file mode 100644 index 0000000000..e141cb1a02 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft.ts @@ -0,0 +1,28 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useMemo } from 'react'; + +type UseFieldsWidgetGroupsDraftParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useFieldsWidgetGroupsDraft = ({ + pageLayoutId, + widgetId, +}: UseFieldsWidgetGroupsDraftParams): { + draftGroups: FieldsWidgetGroup[]; +} => { + const allDraftGroups = useRecoilComponentValue( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const draftGroups = useMemo( + () => allDraftGroups[widgetId] ?? [], + [allDraftGroups, widgetId], + ); + + return { draftGroups }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts new file mode 100644 index 0000000000..b8bcc9de32 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts @@ -0,0 +1,52 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type UseFieldsWidgetGroupsForDisplayParams = { + widgetId: string; + viewId: string | null; + objectNameSingular: string; +}; + +export const useFieldsWidgetGroupsForDisplay = ({ + widgetId, + viewId, + objectNameSingular, +}: UseFieldsWidgetGroupsForDisplayParams) => { + const isPageLayoutInEditMode = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + ); + + const allDraftGroups = useRecoilComponentValue( + fieldsWidgetGroupsDraftComponentState, + ); + + const viewGroups = useFieldsWidgetGroups({ + viewId, + objectNameSingular, + }); + + const draftGroupsForWidget = allDraftGroups[widgetId]; + const hasDraftGroups = + isDefined(draftGroupsForWidget) && draftGroupsForWidget.length > 0; + + const groups = useMemo(() => { + if (isPageLayoutInEditMode && hasDraftGroups) { + return filterDraftGroupsForDisplay(draftGroupsForWidget); + } + + return viewGroups.groups; + }, [ + isPageLayoutInEditMode, + hasDraftGroups, + draftGroupsForWidget, + viewGroups.groups, + ]); + + return { groups }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts deleted file mode 100644 index a8981495c7..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; -import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext'; -import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems'; -import { filterAndOrderFieldsFromConfiguration } from '@/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration'; -import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; - -export const useFieldsWidgetSectionsWithFields = ( - objectNameSingular: string, -) => { - const isMobile = useIsMobile(); - const { isInRightDrawer } = useLayoutRenderingContext(); - const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } = - useFieldsWidgetFieldMetadataItems({ - objectNameSingular, - }); - const temporaryConfiguration = - useTemporaryFieldsConfiguration(objectNameSingular); - - const context = buildWidgetVisibilityContext({ isMobile, isInRightDrawer }); - - const allFieldMetadataItems = [ - ...legacyActivityTargetFieldMetadataItems, - ...inlineFieldMetadataItems, - ]; - - const sectionsWithFields = filterAndOrderFieldsFromConfiguration({ - configuration: temporaryConfiguration, - availableFieldMetadataItems: allFieldMetadataItems, - context, - }); - - return { sectionsWithFields }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts deleted file mode 100644 index 6322fc4e8b..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields'; - -export const useFieldsWidgetSectionsWithIndices = ( - objectNameSingular: string, -) => { - const { sectionsWithFields } = - useFieldsWidgetSectionsWithFields(objectNameSingular); - - const sectionsWithFieldIndices = sectionsWithFields.map( - (section, sectionIndex) => { - const startIndex = sectionsWithFields - .slice(0, sectionIndex) - .reduce((sum, s) => sum + s.fields.length, 0); - - return { - ...section, - fields: section.fields.map((field, fieldIndex) => ({ - field, - globalIndex: startIndex + fieldIndex, - })), - }; - }, - ); - - return { sectionsWithFieldIndices }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useInitializeFieldsWidgetGroupsDraft.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useInitializeFieldsWidgetGroupsDraft.ts new file mode 100644 index 0000000000..97d13da21d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useInitializeFieldsWidgetGroupsDraft.ts @@ -0,0 +1,84 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState'; +import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useEffect } from 'react'; +import { useRecoilCallback } from 'recoil'; + +type UseInitializeFieldsWidgetGroupsDraftParams = { + pageLayoutId: string; + widgetId: string; + serverGroups: FieldsWidgetGroup[]; +}; + +export const useInitializeFieldsWidgetGroupsDraft = ({ + pageLayoutId, + widgetId, + serverGroups, +}: UseInitializeFieldsWidgetGroupsDraftParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState( + fieldsWidgetGroupsPersistedComponentState, + pageLayoutId, + ); + + const hasInitializedFieldsWidgetGroupsDraftState = + useRecoilComponentCallbackState( + hasInitializedFieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const initializeDraft = useRecoilCallback( + ({ set, snapshot }) => + () => { + const hasInitialized = snapshot + .getLoadable(hasInitializedFieldsWidgetGroupsDraftState) + .getValue(); + + if (hasInitialized[widgetId]) { + return; + } + + const currentDraft = snapshot + .getLoadable(fieldsWidgetGroupsDraftState) + .getValue(); + + const hasDraftForWidget = widgetId in currentDraft; + + if (!hasDraftForWidget) { + set(fieldsWidgetGroupsDraftState, (prev) => ({ + ...prev, + [widgetId]: serverGroups, + })); + + set(fieldsWidgetGroupsPersistedState, (prev) => ({ + ...prev, + [widgetId]: serverGroups, + })); + } + + set(hasInitializedFieldsWidgetGroupsDraftState, (prev) => ({ + ...prev, + [widgetId]: true, + })); + }, + [ + hasInitializedFieldsWidgetGroupsDraftState, + fieldsWidgetGroupsDraftState, + fieldsWidgetGroupsPersistedState, + widgetId, + serverGroups, + ], + ); + + useEffect(() => { + if (serverGroups.length > 0) { + initializeDraft(); + } + }, [serverGroups, initializeDraft]); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useMoveFieldInDraft.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useMoveFieldInDraft.ts new file mode 100644 index 0000000000..1a43885706 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useMoveFieldInDraft.ts @@ -0,0 +1,126 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; + +type UseMoveFieldInDraftParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useMoveFieldInDraft = ({ + pageLayoutId, + widgetId, +}: UseMoveFieldInDraftParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const moveFieldRecoilCallback = useRecoilCallback( + ({ set }) => + ( + sourceGroupId: string, + destinationGroupId: string, + sourceIndex: number, + destinationIndex: number, + ) => { + set(fieldsWidgetGroupsDraftState, (prev) => { + const currentGroups = prev[widgetId] ?? []; + + const sourceGroup = currentGroups.find((g) => g.id === sourceGroupId); + + if (!sourceGroup) { + return prev; + } + + const sortedSourceFields = [...sourceGroup.fields].sort( + (a, b) => a.position - b.position, + ); + + const [movedField] = sortedSourceFields.splice(sourceIndex, 1); + + if (!movedField) { + return prev; + } + + if (sourceGroupId === destinationGroupId) { + sortedSourceFields.splice(destinationIndex, 0, movedField); + + const updatedFields = sortedSourceFields.map((field, index) => ({ + ...field, + position: index, + })); + + return { + ...prev, + [widgetId]: currentGroups.map((group) => + group.id === sourceGroupId + ? { ...group, fields: updatedFields } + : group, + ), + }; + } + + const destinationGroup = currentGroups.find( + (g) => g.id === destinationGroupId, + ); + + if (!destinationGroup) { + return prev; + } + + const updatedSourceFields = sortedSourceFields.map( + (field, index) => ({ + ...field, + position: index, + }), + ); + + const sortedDestFields = [...destinationGroup.fields].sort( + (a, b) => a.position - b.position, + ); + sortedDestFields.splice(destinationIndex, 0, movedField); + + const updatedDestFields = sortedDestFields.map((field, index) => ({ + ...field, + position: index, + })); + + return { + ...prev, + [widgetId]: currentGroups.map((group) => { + if (group.id === sourceGroupId) { + return { ...group, fields: updatedSourceFields }; + } + + if (group.id === destinationGroupId) { + return { ...group, fields: updatedDestFields }; + } + + return group; + }), + }; + }); + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const moveField = useCallback( + ( + sourceGroupId: string, + destinationGroupId: string, + sourceIndex: number, + destinationIndex: number, + ) => + moveFieldRecoilCallback( + sourceGroupId, + destinationGroupId, + sourceIndex, + destinationIndex, + ), + [moveFieldRecoilCallback], + ); + + return { moveField }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups.ts new file mode 100644 index 0000000000..57ece347d2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups.ts @@ -0,0 +1,54 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; + +type UseReorderFieldsWidgetEditorGroupsParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useReorderFieldsWidgetEditorGroups = ({ + pageLayoutId, + widgetId, +}: UseReorderFieldsWidgetEditorGroupsParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const reorderGroupsRecoilCallback = useRecoilCallback( + ({ set }) => + (reorderedGroupIds: string[]) => { + set(fieldsWidgetGroupsDraftState, (prev) => { + const currentGroups = prev[widgetId] ?? []; + + const reorderedGroups = reorderedGroupIds + .map((groupId, index) => { + const group = currentGroups.find((g) => g.id === groupId); + + if (!group) { + return null; + } + + return { ...group, position: index }; + }) + .filter((g): g is NonNullable => g !== null); + + return { + ...prev, + [widgetId]: reorderedGroups, + }; + }); + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const reorderGroups = useCallback( + (reorderedGroupIds: string[]) => + reorderGroupsRecoilCallback(reorderedGroupIds), + [reorderGroupsRecoilCallback], + ); + + return { reorderGroups }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft.ts new file mode 100644 index 0000000000..7ff1d7b24c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft.ts @@ -0,0 +1,53 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; + +type UseToggleFieldVisibilityInDraftParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useToggleFieldVisibilityInDraft = ({ + pageLayoutId, + widgetId, +}: UseToggleFieldVisibilityInDraftParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const toggleFieldVisibilityRecoilCallback = useRecoilCallback( + ({ set }) => + (groupId: string, fieldMetadataId: string) => { + set(fieldsWidgetGroupsDraftState, (prev) => { + const currentGroups = prev[widgetId] ?? []; + + return { + ...prev, + [widgetId]: currentGroups.map((group) => + group.id === groupId + ? { + ...group, + fields: group.fields.map((field) => + field.fieldMetadataItem.id === fieldMetadataId + ? { ...field, isVisible: !field.isVisible } + : field, + ), + } + : group, + ), + }; + }); + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const toggleFieldVisibility = useCallback( + (groupId: string, fieldMetadataId: string) => + toggleFieldVisibilityRecoilCallback(groupId, fieldMetadataId), + [toggleFieldVisibilityRecoilCallback], + ); + + return { toggleFieldVisibility }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup.ts new file mode 100644 index 0000000000..3ca66e83a5 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup.ts @@ -0,0 +1,58 @@ +import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useCallback } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +type UpdateGroupParams = { + groupId: string; + name?: string; + position?: number; + isVisible?: boolean; +}; + +type UseUpdateFieldsWidgetEditorGroupParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useUpdateFieldsWidgetEditorGroup = ({ + pageLayoutId, + widgetId, +}: UseUpdateFieldsWidgetEditorGroupParams) => { + const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState( + fieldsWidgetGroupsDraftComponentState, + pageLayoutId, + ); + + const updateGroupRecoilCallback = useRecoilCallback( + ({ set }) => + ({ groupId, name, position, isVisible }: UpdateGroupParams) => { + set(fieldsWidgetGroupsDraftState, (prev) => { + const currentGroups = prev[widgetId] ?? []; + + return { + ...prev, + [widgetId]: currentGroups.map((group) => + group.id === groupId + ? { + ...group, + ...(isDefined(name) ? { name } : {}), + ...(isDefined(position) ? { position } : {}), + ...(isDefined(isVisible) ? { isVisible } : {}), + } + : group, + ), + }; + }); + }, + [fieldsWidgetGroupsDraftState, widgetId], + ); + + const updateGroup = useCallback( + (params: UpdateGroupParams) => updateGroupRecoilCallback(params), + [updateGroupRecoilCallback], + ); + + return { updateGroup }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/types/FieldsWidgetGroup.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/types/FieldsWidgetGroup.ts new file mode 100644 index 0000000000..06dc199ea8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/types/FieldsWidgetGroup.ts @@ -0,0 +1,17 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; + +export type FieldsWidgetGroupField = { + fieldMetadataItem: FieldMetadataItem; + position: number; + isVisible: boolean; + globalIndex: number; + viewFieldId?: string; +}; + +export type FieldsWidgetGroup = { + id: string; + name: string; + position: number; + isVisible: boolean; + fields: FieldsWidgetGroupField[]; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/__tests__/filterDraftGroupsForDisplay.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/__tests__/filterDraftGroupsForDisplay.test.ts new file mode 100644 index 0000000000..16d26c7d98 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/__tests__/filterDraftGroupsForDisplay.test.ts @@ -0,0 +1,187 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay'; + +const mockFieldMetadataItem = { + id: 'field-meta-1', + name: 'name', + label: 'Name', + type: 'TEXT', +} as FieldMetadataItem; + +const makeGroup = ( + overrides: Partial & { id: string }, +): FieldsWidgetGroup => ({ + name: 'Group', + position: 0, + isVisible: true, + fields: [], + ...overrides, +}); + +const makeField = ( + overrides: Partial = {}, +): FieldsWidgetGroup['fields'][number] => ({ + fieldMetadataItem: mockFieldMetadataItem, + position: 0, + isVisible: true, + globalIndex: 0, + viewFieldId: 'vf1', + ...overrides, +}); + +describe('filterDraftGroupsForDisplay', () => { + it('should return empty array for empty input', () => { + const result = filterDraftGroupsForDisplay([]); + + expect(result).toEqual([]); + }); + + it('should filter out non-visible groups', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + isVisible: true, + fields: [makeField()], + }), + makeGroup({ + id: 'g2', + isVisible: false, + fields: [makeField({ viewFieldId: 'vf2' })], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('g1'); + }); + + it('should filter out non-visible fields within groups', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + makeField({ isVisible: true, viewFieldId: 'vf1' }), + makeField({ isVisible: false, viewFieldId: 'vf2' }), + makeField({ isVisible: true, viewFieldId: 'vf3' }), + ], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result).toHaveLength(1); + expect(result[0].fields).toHaveLength(2); + expect(result[0].fields[0].viewFieldId).toBe('vf1'); + expect(result[0].fields[1].viewFieldId).toBe('vf3'); + }); + + it('should remove groups where all fields are hidden', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [makeField({ isVisible: false })], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result).toHaveLength(0); + }); + + it('should sort groups by position', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g3', + position: 2, + fields: [makeField({ viewFieldId: 'vf3' })], + }), + makeGroup({ + id: 'g1', + position: 0, + fields: [makeField({ viewFieldId: 'vf1' })], + }), + makeGroup({ + id: 'g2', + position: 1, + fields: [makeField({ viewFieldId: 'vf2' })], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result.map((g) => g.id)).toEqual(['g1', 'g2', 'g3']); + }); + + it('should sort fields by position within each group', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + fields: [ + makeField({ position: 2, viewFieldId: 'vf2' }), + makeField({ position: 0, viewFieldId: 'vf0' }), + makeField({ position: 1, viewFieldId: 'vf1' }), + ], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result[0].fields.map((f) => f.viewFieldId)).toEqual([ + 'vf0', + 'vf1', + 'vf2', + ]); + }); + + it('should compute correct globalIndex across groups', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + position: 0, + fields: [ + makeField({ position: 0, viewFieldId: 'vf1' }), + makeField({ position: 1, viewFieldId: 'vf2' }), + ], + }), + makeGroup({ + id: 'g2', + position: 1, + fields: [makeField({ position: 0, viewFieldId: 'vf3' })], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result[0].fields[0].globalIndex).toBe(0); + expect(result[0].fields[1].globalIndex).toBe(1); + expect(result[1].fields[0].globalIndex).toBe(2); + }); + + it('should not count hidden fields in globalIndex', () => { + const groups: FieldsWidgetGroup[] = [ + makeGroup({ + id: 'g1', + position: 0, + fields: [ + makeField({ position: 0, isVisible: true, viewFieldId: 'vf1' }), + makeField({ position: 1, isVisible: false, viewFieldId: 'vf2' }), + ], + }), + makeGroup({ + id: 'g2', + position: 1, + fields: [ + makeField({ position: 0, isVisible: true, viewFieldId: 'vf3' }), + ], + }), + ]; + + const result = filterDraftGroupsForDisplay(groups); + + expect(result[0].fields).toHaveLength(1); + expect(result[0].fields[0].globalIndex).toBe(0); + expect(result[1].fields[0].globalIndex).toBe(1); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts deleted file mode 100644 index 0221c67d80..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; -import { type WidgetVisibilityContext } from '@/page-layout/types/WidgetVisibilityContext'; -import { evaluateWidgetVisibility } from '@/page-layout/utils/evaluateWidgetVisibility'; -import { isDefined } from 'twenty-shared/utils'; - -export type FieldsConfigurationSectionWithFields = { - id: string; - title: string; - position: number; - fields: FieldMetadataItem[]; -}; - -type FilterAndOrderFieldsFromConfigurationParams = { - configuration: FieldsConfiguration; - availableFieldMetadataItems: FieldMetadataItem[]; - context: WidgetVisibilityContext; -}; - -export const filterAndOrderFieldsFromConfiguration = ({ - configuration, - availableFieldMetadataItems, - context, -}: FilterAndOrderFieldsFromConfigurationParams): FieldsConfigurationSectionWithFields[] => { - const sortedSections = [...configuration.sections].sort( - (a, b) => a.position - b.position, - ); - - const sectionsWithFields = sortedSections - .map((section) => { - const sectionFieldIds = new Set( - section.fields.map((f) => f.fieldMetadataId), - ); - - const visibleFields = availableFieldMetadataItems - .filter((fieldMetadataItem) => - sectionFieldIds.has(fieldMetadataItem.id), - ) - .filter((fieldMetadataItem) => { - const fieldConfig = section.fields.find( - (f) => f.fieldMetadataId === fieldMetadataItem.id, - ); - - if (!isDefined(fieldConfig)) { - return false; - } - - // Check user-controlled visibility flag - // TODO: This will be replaced by proper conditional display later - if (fieldConfig.isVisible === false) { - return false; - } - - return evaluateWidgetVisibility({ - conditionalDisplay: fieldConfig.conditionalDisplay, - context, - }); - }); - - if (visibleFields.length === 0) { - return null; - } - - return { - id: section.id, - title: section.title, - position: section.position, - fields: visibleFields, - }; - }) - .filter(isDefined); - - return sectionsWithFields; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay.ts new file mode 100644 index 0000000000..a389aa03d2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay.ts @@ -0,0 +1,27 @@ +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; + +export const filterDraftGroupsForDisplay = ( + draftGroups: FieldsWidgetGroup[], +): FieldsWidgetGroup[] => { + const sortedGroups = [...draftGroups].sort((a, b) => a.position - b.position); + + let globalIndex = 0; + + return sortedGroups + .filter((group) => group.isVisible) + .map((group) => { + const visibleFields = [...group.fields] + .sort((a, b) => a.position - b.position) + .filter((field) => field.isVisible) + .map((field) => ({ + ...field, + globalIndex: globalIndex++, + })); + + return { + ...group, + fields: visibleFields, + }; + }) + .filter((group) => group.fields.length > 0); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/hasMinimalRequiredConfigForGraph.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/hasMinimalRequiredConfigForGraph.ts index 824718233a..56f0ac1cb6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/hasMinimalRequiredConfigForGraph.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/hasMinimalRequiredConfigForGraph.ts @@ -1,8 +1,10 @@ import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType'; import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration'; -import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; import { isDefined } from 'twenty-shared/utils'; -import { type WidgetConfiguration } from '~/generated-metadata/graphql'; +import { + type FieldsConfiguration, + type WidgetConfiguration, +} from '~/generated-metadata/graphql'; export const hasMinimalRequiredConfigForGraph = ( configuration: WidgetConfiguration | FieldsConfiguration | FieldConfiguration, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts new file mode 100644 index 0000000000..44ceccac1d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts @@ -0,0 +1,91 @@ +import { getWidgetCardVariant } from '@/page-layout/widgets/utils/getWidgetCardVariant'; +import { + PageLayoutTabLayoutMode, + PageLayoutType, +} from '~/generated-metadata/graphql'; + +describe('getWidgetCardVariant', () => { + it('should return dashboard for DASHBOARD page layout type', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.GRID, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.DASHBOARD, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('dashboard'); + }); + + it('should return canvas for CANVAS layout mode', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.CANVAS, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.RECORD_PAGE, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('canvas'); + }); + + it('should return side-column when isInPinnedTab is true', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.GRID, + isInPinnedTab: true, + pageLayoutType: PageLayoutType.RECORD_PAGE, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('side-column'); + }); + + it('should return side-column when isMobile is true', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.GRID, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.RECORD_PAGE, + isMobile: true, + isInRightDrawer: false, + }), + ).toBe('side-column'); + }); + + it('should return side-column when isInRightDrawer is true', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.GRID, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.RECORD_PAGE, + isMobile: false, + isInRightDrawer: true, + }), + ).toBe('side-column'); + }); + + it('should return record-page as default', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.GRID, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.RECORD_PAGE, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('record-page'); + }); + + it('should prioritize dashboard over canvas', () => { + expect( + getWidgetCardVariant({ + layoutMode: PageLayoutTabLayoutMode.CANVAS, + isInPinnedTab: false, + pageLayoutType: PageLayoutType.DASHBOARD, + isMobile: false, + isInRightDrawer: false, + }), + ).toBe('dashboard'); + }); +}); diff --git a/packages/twenty-front/src/modules/users/components/MetadataProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/MetadataProviderEffect.tsx index d18ebb7a04..448c9289bc 100644 --- a/packages/twenty-front/src/modules/users/components/MetadataProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/MetadataProviderEffect.tsx @@ -26,11 +26,13 @@ import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { AppPath, type ObjectPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { - type WorkspaceMember, + ViewType as CoreViewType, useFindAllCoreViewsQuery, useFindAllRecordPageLayoutsQuery, - useGetCurrentUserQuery, + useFindFieldsWidgetCoreViewsQuery, useFindManyLogicFunctionsQuery, + useGetCurrentUserQuery, + type WorkspaceMember, } from '~/generated-metadata/graphql'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2'; @@ -38,6 +40,14 @@ import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; +const INDEX_VIEW_TYPES = [ + CoreViewType.TABLE, + CoreViewType.KANBAN, + CoreViewType.CALENDAR, +]; + +const FIELDS_WIDGET_VIEW_TYPES = [CoreViewType.FIELDS_WIDGET]; + export const MetadataProviderEffect = () => { const location = useLocation(); @@ -77,15 +87,41 @@ export const MetadataProviderEffect = () => { [store], ); - const setCoreViews = useRecoilCallback( + const setIndexCoreViews = useRecoilCallback( ({ set, snapshot }) => - (coreViews: CoreViewWithRelations[]) => { + (indexViews: CoreViewWithRelations[]) => { const existingCoreViews = snapshot .getLoadable(coreViewsState) .getValue(); - if (!isDeeplyEqual(existingCoreViews, coreViews)) { - set(coreViewsState, coreViews); + const existingFieldsWidgetViews = existingCoreViews.filter( + (view) => view.type === CoreViewType.FIELDS_WIDGET, + ); + + const mergedViews = [...indexViews, ...existingFieldsWidgetViews]; + + if (!isDeeplyEqual(existingCoreViews, mergedViews)) { + set(coreViewsState, mergedViews); + } + }, + [], + ); + + const setFieldsWidgetCoreViews = useRecoilCallback( + ({ set, snapshot }) => + (fieldsWidgetViews: CoreViewWithRelations[]) => { + const existingCoreViews = snapshot + .getLoadable(coreViewsState) + .getValue(); + + const existingIndexViews = existingCoreViews.filter( + (view) => view.type !== CoreViewType.FIELDS_WIDGET, + ); + + const mergedViews = [...existingIndexViews, ...fieldsWidgetViews]; + + if (!isDeeplyEqual(existingCoreViews, mergedViews)) { + set(coreViewsState, mergedViews); } }, [], @@ -129,6 +165,13 @@ export const MetadataProviderEffect = () => { const { data: queryDataCoreViews, loading: queryLoadingCoreViews } = useFindAllCoreViewsQuery({ skip: shouldSkip, + variables: { viewTypes: INDEX_VIEW_TYPES }, + }); + + const { data: queryDataFieldsWidgetCoreViews } = + useFindFieldsWidgetCoreViewsQuery({ + skip: shouldSkip, + variables: { viewTypes: FIELDS_WIDGET_VIEW_TYPES }, }); const { data: queryDataRecordPageLayouts } = useFindAllRecordPageLayoutsQuery( @@ -239,7 +282,7 @@ export const MetadataProviderEffect = () => { initializeFormatPreferences, setCurrentWorkspaceMembersWithDeleted, updateLocaleCatalog, - setCoreViews, + setIndexCoreViews, ]); useEffect(() => { @@ -249,8 +292,18 @@ export const MetadataProviderEffect = () => { if (!isDefined(queryDataCoreViews?.getCoreViews)) return; - setCoreViews(queryDataCoreViews.getCoreViews); - }, [queryDataCoreViews?.getCoreViews, setCoreViews, queryLoadingCoreViews]); + setIndexCoreViews(queryDataCoreViews.getCoreViews); + }, [ + queryDataCoreViews?.getCoreViews, + setIndexCoreViews, + queryLoadingCoreViews, + ]); + + useEffect(() => { + if (!isDefined(queryDataFieldsWidgetCoreViews?.getCoreViews)) return; + + setFieldsWidgetCoreViews(queryDataFieldsWidgetCoreViews.getCoreViews); + }, [queryDataFieldsWidgetCoreViews?.getCoreViews, setFieldsWidgetCoreViews]); useEffect(() => { if (!isDefined(queryDataRecordPageLayouts?.getPageLayouts)) return; diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldGroupFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldGroupFragment.ts new file mode 100644 index 0000000000..c6357811b5 --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewFieldGroupFragment.ts @@ -0,0 +1,19 @@ +import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment'; +import { gql } from '@apollo/client'; + +export const VIEW_FIELD_GROUP_FRAGMENT = gql` + ${VIEW_FIELD_FRAGMENT} + fragment ViewFieldGroupFragment on CoreViewFieldGroup { + id + name + position + isVisible + viewId + createdAt + updatedAt + deletedAt + viewFields { + ...ViewFieldFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts index 226bf7acd2..44c631f5ab 100644 --- a/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts @@ -1,4 +1,5 @@ import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment'; +import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment'; import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment'; import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment'; @@ -6,6 +7,7 @@ import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment'; import { gql } from '@apollo/client'; export const VIEW_FRAGMENT = gql` + ${VIEW_FIELD_GROUP_FRAGMENT} ${VIEW_FIELD_FRAGMENT} ${VIEW_FILTER_FRAGMENT} ${VIEW_FILTER_GROUP_FRAGMENT} @@ -34,6 +36,9 @@ export const VIEW_FRAGMENT = gql` viewFields { ...ViewFieldFragment } + viewFieldGroups { + ...ViewFieldGroupFragment + } viewFilters { ...ViewFilterFragment } diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/createCoreViewFieldGroup.ts b/packages/twenty-front/src/modules/views/graphql/mutations/createCoreViewFieldGroup.ts new file mode 100644 index 0000000000..bf1261b710 --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/mutations/createCoreViewFieldGroup.ts @@ -0,0 +1,11 @@ +import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; +import { gql } from '@apollo/client'; + +export const CREATE_CORE_VIEW_FIELD_GROUP = gql` + ${VIEW_FIELD_GROUP_FRAGMENT} + mutation CreateCoreViewFieldGroup($input: CreateViewFieldGroupInput!) { + createCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewFieldGroups.ts b/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewFieldGroups.ts new file mode 100644 index 0000000000..c0df2c2b31 --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewFieldGroups.ts @@ -0,0 +1,13 @@ +import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; +import { gql } from '@apollo/client'; + +export const CREATE_MANY_CORE_VIEW_FIELD_GROUPS = gql` + ${VIEW_FIELD_GROUP_FRAGMENT} + mutation CreateManyCoreViewFieldGroups( + $inputs: [CreateViewFieldGroupInput!]! + ) { + createManyCoreViewFieldGroups(inputs: $inputs) { + ...ViewFieldGroupFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewFieldGroup.ts b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewFieldGroup.ts new file mode 100644 index 0000000000..056413176a --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewFieldGroup.ts @@ -0,0 +1,11 @@ +import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; +import { gql } from '@apollo/client'; + +export const DELETE_CORE_VIEW_FIELD_GROUP = gql` + ${VIEW_FIELD_GROUP_FRAGMENT} + mutation DeleteCoreViewFieldGroup($input: DeleteViewFieldGroupInput!) { + deleteCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewFieldGroup.ts b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewFieldGroup.ts new file mode 100644 index 0000000000..9b288cbbca --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewFieldGroup.ts @@ -0,0 +1,11 @@ +import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; +import { gql } from '@apollo/client'; + +export const UPDATE_CORE_VIEW_FIELD_GROUP = gql` + ${VIEW_FIELD_GROUP_FRAGMENT} + mutation UpdateCoreViewFieldGroup($input: UpdateViewFieldGroupInput!) { + updateCoreViewFieldGroup(input: $input) { + ...ViewFieldGroupFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/queries/findAllCoreViews.ts b/packages/twenty-front/src/modules/views/graphql/queries/findAllCoreViews.ts index eb04d662de..131b239cf2 100644 --- a/packages/twenty-front/src/modules/views/graphql/queries/findAllCoreViews.ts +++ b/packages/twenty-front/src/modules/views/graphql/queries/findAllCoreViews.ts @@ -3,8 +3,8 @@ import { gql } from '@apollo/client'; export const FIND_ALL_CORE_VIEWS = gql` ${VIEW_FRAGMENT} - query FindAllCoreViews { - getCoreViews { + query FindAllCoreViews($viewTypes: [ViewType!]) { + getCoreViews(viewTypes: $viewTypes) { ...ViewFragment } } diff --git a/packages/twenty-front/src/modules/views/graphql/queries/findFieldsWidgetCoreViews.ts b/packages/twenty-front/src/modules/views/graphql/queries/findFieldsWidgetCoreViews.ts new file mode 100644 index 0000000000..35aba8597a --- /dev/null +++ b/packages/twenty-front/src/modules/views/graphql/queries/findFieldsWidgetCoreViews.ts @@ -0,0 +1,11 @@ +import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment'; +import { gql } from '@apollo/client'; + +export const FIND_FIELDS_WIDGET_CORE_VIEWS = gql` + ${VIEW_FRAGMENT} + query FindFieldsWidgetCoreViews($viewTypes: [ViewType!]) { + getCoreViews(viewTypes: $viewTypes) { + ...ViewFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/views/graphql/queries/findManyCoreViews.ts b/packages/twenty-front/src/modules/views/graphql/queries/findManyCoreViews.ts index d027150f75..bf0f1ff6dd 100644 --- a/packages/twenty-front/src/modules/views/graphql/queries/findManyCoreViews.ts +++ b/packages/twenty-front/src/modules/views/graphql/queries/findManyCoreViews.ts @@ -3,8 +3,8 @@ import { gql } from '@apollo/client'; export const FIND_MANY_CORE_VIEWS = gql` ${VIEW_FRAGMENT} - query FindManyCoreViews($objectMetadataId: String) { - getCoreViews(objectMetadataId: $objectMetadataId) { + query FindManyCoreViews($objectMetadataId: String, $viewTypes: [ViewType!]) { + getCoreViews(objectMetadataId: $objectMetadataId, viewTypes: $viewTypes) { ...ViewFragment } } diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewFieldGroupAPIPersist.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewFieldGroupAPIPersist.ts new file mode 100644 index 0000000000..363abc859d --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewFieldGroupAPIPersist.ts @@ -0,0 +1,253 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { CREATE_MANY_CORE_VIEW_FIELD_GROUPS } from '@/views/graphql/mutations/createManyCoreViewFieldGroups'; +import { DELETE_CORE_VIEW_FIELD_GROUP } from '@/views/graphql/mutations/deleteCoreViewFieldGroup'; +import { UPDATE_CORE_VIEW_FIELD_GROUP } from '@/views/graphql/mutations/updateCoreViewFieldGroup'; +import { useTriggerViewFieldGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldGroupOptimisticEffect'; +import { useMutation, type ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { CrudOperationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { + type CoreViewFieldGroup, + type MutationCreateManyCoreViewFieldGroupsArgs, + type MutationDeleteCoreViewFieldGroupArgs, + type MutationUpdateCoreViewFieldGroupArgs, +} from '~/generated-metadata/graphql'; + +type CreateManyCoreViewFieldGroupsMutationResult = { + createManyCoreViewFieldGroups: CoreViewFieldGroup[]; +}; + +type UpdateCoreViewFieldGroupMutationResult = { + updateCoreViewFieldGroup: CoreViewFieldGroup; +}; + +type DeleteCoreViewFieldGroupMutationResult = { + deleteCoreViewFieldGroup: CoreViewFieldGroup; +}; + +export const usePerformViewFieldGroupAPIPersist = () => { + const { triggerViewFieldGroupOptimisticEffect } = + useTriggerViewFieldGroupOptimisticEffect(); + + const [createManyCoreViewFieldGroupsMutation] = useMutation< + CreateManyCoreViewFieldGroupsMutationResult, + MutationCreateManyCoreViewFieldGroupsArgs + >(CREATE_MANY_CORE_VIEW_FIELD_GROUPS); + const [updateCoreViewFieldGroupMutation] = useMutation< + UpdateCoreViewFieldGroupMutationResult, + MutationUpdateCoreViewFieldGroupArgs + >(UPDATE_CORE_VIEW_FIELD_GROUP); + const [deleteCoreViewFieldGroupMutation] = useMutation< + DeleteCoreViewFieldGroupMutationResult, + MutationDeleteCoreViewFieldGroupArgs + >(DELETE_CORE_VIEW_FIELD_GROUP); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const performViewFieldGroupAPICreate = useCallback( + async ( + createCoreViewFieldGroupInputs: MutationCreateManyCoreViewFieldGroupsArgs, + ): Promise< + MetadataRequestResult + > => { + if ( + !Array.isArray(createCoreViewFieldGroupInputs.inputs) || + createCoreViewFieldGroupInputs.inputs.length === 0 + ) { + return { + status: 'successful', + response: null, + }; + } + + try { + const result = await createManyCoreViewFieldGroupsMutation({ + variables: createCoreViewFieldGroupInputs, + update: (_cache, { data }) => { + const createdViewFieldGroups = data?.createManyCoreViewFieldGroups; + if (!isDefined(createdViewFieldGroups)) { + return; + } + + triggerViewFieldGroupOptimisticEffect({ + createdViewFieldGroups, + }); + }, + }); + + return { + status: 'successful', + response: result.data ?? null, + }; + } catch (error) { + if ( + typeof error === 'object' && + error !== null && + 'graphQLErrors' in error + ) { + handleMetadataError(error as ApolloError, { + primaryMetadataName: 'viewFieldGroup', + operationType: CrudOperationType.CREATE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldGroupOptimisticEffect, + createManyCoreViewFieldGroupsMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const performViewFieldGroupAPIUpdate = useCallback( + async ( + updateCoreViewFieldGroupInputs: MutationUpdateCoreViewFieldGroupArgs[], + ): Promise< + MetadataRequestResult + > => { + if (updateCoreViewFieldGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + updateCoreViewFieldGroupInputs.map((variables) => + updateCoreViewFieldGroupMutation({ + variables, + update: (_cache, { data }) => { + const updatedViewFieldGroup = data?.updateCoreViewFieldGroup; + if (!isDefined(updatedViewFieldGroup)) { + return; + } + + triggerViewFieldGroupOptimisticEffect({ + updatedViewFieldGroups: [updatedViewFieldGroup], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results + .map((r) => r.data) + .filter(isDefined) as UpdateCoreViewFieldGroupMutationResult[], + }; + } catch (error) { + if ( + typeof error === 'object' && + error !== null && + 'graphQLErrors' in error + ) { + handleMetadataError(error as ApolloError, { + primaryMetadataName: 'viewFieldGroup', + operationType: CrudOperationType.UPDATE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldGroupOptimisticEffect, + updateCoreViewFieldGroupMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const performViewFieldGroupAPIDelete = useCallback( + async ( + deleteCoreViewFieldGroupInputs: MutationDeleteCoreViewFieldGroupArgs[], + ): Promise< + MetadataRequestResult + > => { + if (deleteCoreViewFieldGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + deleteCoreViewFieldGroupInputs.map((variables) => + deleteCoreViewFieldGroupMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewFieldGroup = data?.deleteCoreViewFieldGroup; + if (!isDefined(deletedViewFieldGroup)) { + return; + } + + triggerViewFieldGroupOptimisticEffect({ + deletedViewFieldGroups: [deletedViewFieldGroup], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results + .map((r) => r.data) + .filter(isDefined) as DeleteCoreViewFieldGroupMutationResult[], + }; + } catch (error) { + if ( + typeof error === 'object' && + error !== null && + 'graphQLErrors' in error + ) { + handleMetadataError(error as ApolloError, { + primaryMetadataName: 'viewFieldGroup', + operationType: CrudOperationType.DELETE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldGroupOptimisticEffect, + deleteCoreViewFieldGroupMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + return { + performViewFieldGroupAPICreate, + performViewFieldGroupAPIUpdate, + performViewFieldGroupAPIDelete, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateManyViewFieldGroups.ts b/packages/twenty-front/src/modules/views/hooks/useCreateManyViewFieldGroups.ts new file mode 100644 index 0000000000..b7f90cf88a --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useCreateManyViewFieldGroups.ts @@ -0,0 +1,22 @@ +import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; + +type CreateViewFieldGroupInput = { + id: string; + name: string; + position: number; + isVisible: boolean; + viewId: string; +}; + +export const useCreateManyViewFieldGroups = () => { + const { performViewFieldGroupAPICreate } = + usePerformViewFieldGroupAPIPersist(); + + const createManyViewFieldGroups = async ( + inputs: CreateViewFieldGroupInput[], + ) => { + return performViewFieldGroupAPICreate({ inputs }); + }; + + return { createManyViewFieldGroups }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFieldGroup.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFieldGroup.ts new file mode 100644 index 0000000000..dd4d7535e6 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFieldGroup.ts @@ -0,0 +1,22 @@ +import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; + +type CreateViewFieldGroupInput = { + id: string; + name: string; + position: number; + isVisible: boolean; + viewId: string; +}; + +export const useCreateViewFieldGroup = () => { + const { performViewFieldGroupAPICreate } = + usePerformViewFieldGroupAPIPersist(); + + const createViewFieldGroup = async (input: CreateViewFieldGroupInput) => { + return performViewFieldGroupAPICreate({ + inputs: [input], + }); + }; + + return { createViewFieldGroup }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useDeleteViewFieldGroup.ts b/packages/twenty-front/src/modules/views/hooks/useDeleteViewFieldGroup.ts new file mode 100644 index 0000000000..9d56268c30 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useDeleteViewFieldGroup.ts @@ -0,0 +1,14 @@ +import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; + +export const useDeleteViewFieldGroup = () => { + const { performViewFieldGroupAPIDelete } = + usePerformViewFieldGroupAPIPersist(); + + const deleteViewFieldGroup = async (viewFieldGroupId: string) => { + return performViewFieldGroupAPIDelete([ + { input: { id: viewFieldGroupId } }, + ]); + }; + + return { deleteViewFieldGroup }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts b/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts new file mode 100644 index 0000000000..717ab69399 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts @@ -0,0 +1,14 @@ +import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; +import { useRecoilValue } from 'recoil'; + +export const useGetViewById = (viewId: string | null) => { + const view = useRecoilValue( + coreViewFromViewIdFamilySelector({ + viewId: viewId ?? '', + }), + ); + + return { + view: viewId ? view : undefined, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useUpdateViewFieldGroup.ts b/packages/twenty-front/src/modules/views/hooks/useUpdateViewFieldGroup.ts new file mode 100644 index 0000000000..4d016fe730 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useUpdateViewFieldGroup.ts @@ -0,0 +1,35 @@ +import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; + +type UpdateViewFieldGroupInput = { + id: string; + name?: string; + position?: number; + isVisible?: boolean; +}; + +export const useUpdateViewFieldGroup = () => { + const { performViewFieldGroupAPIUpdate } = + usePerformViewFieldGroupAPIPersist(); + + const updateViewFieldGroup = async ({ + id, + name, + position, + isVisible, + }: UpdateViewFieldGroupInput) => { + return performViewFieldGroupAPIUpdate([ + { + input: { + id, + update: { + name, + position, + isVisible, + }, + }, + }, + ]); + }; + + return { updateViewFieldGroup }; +}; diff --git a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldGroupOptimisticEffect.ts b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldGroupOptimisticEffect.ts new file mode 100644 index 0000000000..bb4c8fa560 --- /dev/null +++ b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewFieldGroupOptimisticEffect.ts @@ -0,0 +1,162 @@ +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { coreViewsState } from '@/views/states/coreViewState'; +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; +import { useApolloClient } from '@apollo/client'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { type CoreViewFieldGroup } from '~/generated-metadata/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +type ViewFieldGroupForOptimisticEffect = Omit< + CoreViewFieldGroup, + 'workspaceId' +>; + +export const useTriggerViewFieldGroupOptimisticEffect = () => { + const apolloClient = useApolloClient(); + + const cache = apolloClient.cache; + + const triggerViewFieldGroupOptimisticEffect = useRecoilCallback( + ({ set, snapshot }) => + ({ + createdViewFieldGroups = [], + updatedViewFieldGroups = [], + deletedViewFieldGroups = [], + }: { + createdViewFieldGroups?: ViewFieldGroupForOptimisticEffect[]; + updatedViewFieldGroups?: ViewFieldGroupForOptimisticEffect[]; + deletedViewFieldGroups?: Pick[]; + }) => { + const coreViews = getSnapshotValue(snapshot, coreViewsState); + let newCoreViews = [...coreViews]; + + createdViewFieldGroups.forEach((createdViewFieldGroup) => { + cache.modify({ + id: cache.identify({ + __typename: 'CoreView', + id: createdViewFieldGroup.viewId, + }), + fields: { + viewFieldGroups: (existingViewFieldGroups, { toReference }) => [ + ...(existingViewFieldGroups ?? []), + toReference(createdViewFieldGroup), + ], + }, + }); + const toBeModifiedCoreView = newCoreViews.find( + (coreView) => coreView.id === createdViewFieldGroup.viewId, + ); + if (isDefined(toBeModifiedCoreView)) { + newCoreViews = [ + ...newCoreViews.filter( + (coreView) => coreView.id !== createdViewFieldGroup.viewId, + ), + { + ...toBeModifiedCoreView, + viewFieldGroups: [ + ...(toBeModifiedCoreView.viewFieldGroups ?? []), + createdViewFieldGroup, + ], + }, + ]; + } + }); + + updatedViewFieldGroups.forEach((updatedViewFieldGroup) => { + cache.modify({ + id: cache.identify({ + __typename: 'CoreView', + id: updatedViewFieldGroup.viewId, + }), + fields: { + viewFieldGroups: ( + existingViewFieldGroups, + { readField, toReference }, + ) => + existingViewFieldGroups?.map((viewFieldGroup) => { + const viewFieldGroupId = readField( + 'id', + viewFieldGroup, + ); + if (viewFieldGroupId === updatedViewFieldGroup.id) { + return toReference(updatedViewFieldGroup); + } + return viewFieldGroup; + }) ?? [], + }, + }); + const toBeModifiedCoreView = newCoreViews.find( + (coreView) => coreView.id === updatedViewFieldGroup.viewId, + ); + if (isDefined(toBeModifiedCoreView)) { + newCoreViews = [ + ...newCoreViews.filter( + (coreView) => coreView.id !== updatedViewFieldGroup.viewId, + ), + { + ...toBeModifiedCoreView, + viewFieldGroups: [ + ...(toBeModifiedCoreView.viewFieldGroups ?? []).filter( + (viewFieldGroup) => + viewFieldGroup.id !== updatedViewFieldGroup.id, + ), + updatedViewFieldGroup, + ], + }, + ]; + } + }); + + deletedViewFieldGroups.forEach( + ( + deletedViewFieldGroup: Pick, + ) => { + cache.modify({ + id: cache.identify({ + __typename: 'CoreView', + id: deletedViewFieldGroup.viewId, + }), + fields: { + viewFieldGroups: (existingViewFieldGroups, { readField }) => + existingViewFieldGroups?.filter( + (viewFieldGroup) => + readField('id', viewFieldGroup) !== + deletedViewFieldGroup.id, + ) ?? [], + }, + }); + const toBeModifiedCoreView = newCoreViews.find( + (coreView) => coreView.id === deletedViewFieldGroup.viewId, + ); + + if (isDefined(toBeModifiedCoreView)) { + newCoreViews = [ + ...newCoreViews.filter( + (coreView) => coreView.id !== deletedViewFieldGroup.viewId, + ), + { + ...toBeModifiedCoreView, + viewFieldGroups: ( + toBeModifiedCoreView.viewFieldGroups ?? [] + ).filter( + (viewFieldGroup) => + viewFieldGroup.id !== deletedViewFieldGroup.id, + ), + }, + ]; + } + }, + ); + + if (!isDeeplyEqual(coreViews, newCoreViews)) { + set(coreViewsState, newCoreViews); + } + }, + [cache], + ); + + return { + triggerViewFieldGroupOptimisticEffect, + }; +}; diff --git a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts index 690a322131..d7fd848048 100644 --- a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts +++ b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts @@ -2,6 +2,7 @@ import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential' import { type AggregateOperations, type CoreViewField, + type CoreViewFieldGroup, type CoreViewFilter, type CoreViewFilterGroup, type CoreViewGroup, @@ -12,6 +13,18 @@ import { type ViewVisibility, } from '~/generated-metadata/graphql'; +export type CoreViewFieldEssential = Omit< + CoreViewField, + 'workspaceId' | 'createdAt' | 'updatedAt' +>; + +export type CoreViewFieldGroupEssential = Omit< + CoreViewFieldGroup, + 'workspaceId' | 'createdAt' | 'updatedAt' | 'viewFields' +> & { + viewFields: CoreViewFieldEssential[]; +}; + export type CoreViewWithRelations = { id: string; name: string; @@ -19,7 +32,8 @@ export type CoreViewWithRelations = { key?: ViewKey | null; objectMetadataId: string; isCompact: boolean; - viewFields: Omit[]; + viewFields: CoreViewFieldEssential[]; + viewFieldGroups?: CoreViewFieldGroupEssential[]; viewGroups: Omit[]; viewFilters: Omit< CoreViewFilter, diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index f8f2e617ce..456f5d3c23 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -1,6 +1,7 @@ import { type AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { type ViewField } from '@/views/types/ViewField'; +import { type ViewFieldGroup } from '@/views/types/ViewFieldGroup'; import { type ViewFilter } from '@/views/types/ViewFilter'; import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup'; import { type ViewGroup } from '@/views/types/ViewGroup'; @@ -8,8 +9,8 @@ import { type ViewKey } from '@/views/types/ViewKey'; import { type ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { type ViewType } from '@/views/types/ViewType'; import { - type ViewVisibility, type ViewCalendarLayout, + type ViewVisibility, } from '~/generated-metadata/graphql'; export type View = { @@ -20,6 +21,7 @@ export type View = { objectMetadataId: string; isCompact: boolean; viewFields: ViewField[]; + viewFieldGroups?: ViewFieldGroup[]; viewGroups: ViewGroup[]; viewFilters: ViewFilter[]; viewFilterGroups?: ViewFilterGroup[]; diff --git a/packages/twenty-front/src/modules/views/types/ViewFieldGroup.ts b/packages/twenty-front/src/modules/views/types/ViewFieldGroup.ts new file mode 100644 index 0000000000..cbfc073226 --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewFieldGroup.ts @@ -0,0 +1,11 @@ +import { type ViewField } from '@/views/types/ViewField'; + +export type ViewFieldGroup = { + __typename: 'ViewFieldGroup'; + id: string; + name: string; + position: number; + isVisible: boolean; + viewId: string; + viewFields: ViewField[]; +}; diff --git a/packages/twenty-front/src/modules/views/types/ViewType.ts b/packages/twenty-front/src/modules/views/types/ViewType.ts index 1a2e1fbb27..1467136584 100644 --- a/packages/twenty-front/src/modules/views/types/ViewType.ts +++ b/packages/twenty-front/src/modules/views/types/ViewType.ts @@ -4,10 +4,12 @@ import { IconLayoutKanban, IconTable, } from 'twenty-ui/display'; + export enum ViewType { Table = 'table', Kanban = 'kanban', Calendar = 'calendar', + FieldsWidget = 'fields_widget', } const VIEW_TYPE_ICON_MAPPING = [ diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewFieldGroupToViewFieldGroup.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewFieldGroupToViewFieldGroup.test.ts new file mode 100644 index 0000000000..c69438a801 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewFieldGroupToViewFieldGroup.test.ts @@ -0,0 +1,105 @@ +import { convertCoreViewFieldGroupToViewFieldGroup } from '@/views/utils/convertCoreViewFieldGroupToViewFieldGroup'; + +describe('convertCoreViewFieldGroupToViewFieldGroup', () => { + it('should convert a core view field group to a view field group', () => { + const coreViewFieldGroup = { + id: 'group-1', + name: 'Group 1', + position: 0, + isVisible: true, + viewId: 'view-1', + viewFields: [ + { + id: 'vf-1', + fieldMetadataId: 'fm-1', + position: 0, + isVisible: true, + size: 150, + aggregateOperation: null, + }, + { + id: 'vf-2', + fieldMetadataId: 'fm-2', + position: 1, + isVisible: false, + size: 200, + aggregateOperation: null, + }, + ], + }; + + const result = + convertCoreViewFieldGroupToViewFieldGroup(coreViewFieldGroup); + + expect(result).toEqual({ + __typename: 'ViewFieldGroup', + id: 'group-1', + name: 'Group 1', + position: 0, + isVisible: true, + viewId: 'view-1', + viewFields: [ + { + __typename: 'ViewField', + id: 'vf-1', + fieldMetadataId: 'fm-1', + position: 0, + isVisible: true, + size: 150, + aggregateOperation: null, + definition: undefined, + }, + { + __typename: 'ViewField', + id: 'vf-2', + fieldMetadataId: 'fm-2', + position: 1, + isVisible: false, + size: 200, + aggregateOperation: null, + definition: undefined, + }, + ], + }); + }); + + it('should handle a group with no view fields', () => { + const coreViewFieldGroup = { + id: 'group-2', + name: 'Empty Group', + position: 1, + isVisible: false, + viewId: 'view-1', + viewFields: [], + }; + + const result = + convertCoreViewFieldGroupToViewFieldGroup(coreViewFieldGroup); + + expect(result).toEqual({ + __typename: 'ViewFieldGroup', + id: 'group-2', + name: 'Empty Group', + position: 1, + isVisible: false, + viewId: 'view-1', + viewFields: [], + }); + }); + + it('should set __typename to ViewFieldGroup', () => { + const coreViewFieldGroup = { + id: 'group-3', + name: 'Test', + position: 0, + isVisible: true, + viewId: 'view-2', + viewFields: [], + }; + + const result = + convertCoreViewFieldGroupToViewFieldGroup(coreViewFieldGroup); + + expect(result.__typename).toBe('ViewFieldGroup'); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewToView.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewToView.test.ts new file mode 100644 index 0000000000..347783eb6f --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewToView.test.ts @@ -0,0 +1,94 @@ +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; +import { + ViewType as CoreViewType, + ViewVisibility, +} from '~/generated-metadata/graphql'; + +const makeCoreView = (overrides = {}) => ({ + id: 'view-1', + name: 'Test View', + type: CoreViewType.TABLE, + key: null, + objectMetadataId: 'obj-1', + isCompact: false, + viewFields: [], + viewFieldGroups: [], + viewGroups: [], + viewFilters: [], + viewFilterGroups: [], + viewSorts: [], + mainGroupByFieldMetadataId: null, + shouldHideEmptyGroups: false, + kanbanAggregateOperation: null, + kanbanAggregateOperationFieldMetadataId: null, + calendarFieldMetadataId: null, + calendarLayout: null, + position: 0, + icon: 'IconTable', + openRecordIn: null, + anyFieldFilterValue: null, + visibility: ViewVisibility.WORKSPACE, + createdByUserWorkspaceId: 'user-1', + ...overrides, +}); + +describe('convertCoreViewToView', () => { + it('should convert a core view to a view with correct typename', () => { + const result = convertCoreViewToView(makeCoreView() as any); + + expect(result.__typename).toBe('View'); + expect(result.id).toBe('view-1'); + expect(result.name).toBe('Test View'); + }); + + it('should map viewFieldGroups through converter', () => { + const coreView = makeCoreView({ + viewFieldGroups: [ + { + id: 'group-1', + name: 'Group 1', + position: 0, + isVisible: true, + viewId: 'view-1', + viewFields: [ + { + id: 'vf-1', + fieldMetadataId: 'fm-1', + position: 0, + isVisible: true, + size: 150, + aggregateOperation: null, + }, + ], + }, + ], + }); + + const result = convertCoreViewToView(coreView as any); + + expect(result.viewFieldGroups).toHaveLength(1); + expect(result.viewFieldGroups![0].__typename).toBe('ViewFieldGroup'); + expect(result.viewFieldGroups![0].id).toBe('group-1'); + expect(result.viewFieldGroups![0].viewFields).toHaveLength(1); + }); + + it('should handle null kanban fields', () => { + const result = convertCoreViewToView( + makeCoreView({ + kanbanAggregateOperation: null, + kanbanAggregateOperationFieldMetadataId: null, + }) as any, + ); + + expect(result.kanbanAggregateOperation).toBeNull(); + expect(result.kanbanAggregateOperationFieldMetadataId).toBeNull(); + }); + + it('should default visibility to UNLISTED when not provided', () => { + const result = convertCoreViewToView( + makeCoreView({ visibility: undefined }) as any, + ); + + expect(result.visibility).toBe(ViewVisibility.UNLISTED); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewTypeToViewType.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewTypeToViewType.test.ts new file mode 100644 index 0000000000..669d7928ae --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/convertCoreViewTypeToViewType.test.ts @@ -0,0 +1,35 @@ +import { ViewType } from '@/views/types/ViewType'; +import { convertCoreViewTypeToViewType } from '@/views/utils/convertCoreViewTypeToViewType'; +import { ViewType as CoreViewType } from '~/generated-metadata/graphql'; + +describe('convertCoreViewTypeToViewType', () => { + it('should convert TABLE to Table', () => { + expect(convertCoreViewTypeToViewType(CoreViewType.TABLE)).toBe( + ViewType.Table, + ); + }); + + it('should convert KANBAN to Kanban', () => { + expect(convertCoreViewTypeToViewType(CoreViewType.KANBAN)).toBe( + ViewType.Kanban, + ); + }); + + it('should convert CALENDAR to Calendar', () => { + expect(convertCoreViewTypeToViewType(CoreViewType.CALENDAR)).toBe( + ViewType.Calendar, + ); + }); + + it('should convert FIELDS_WIDGET to FieldsWidget', () => { + expect(convertCoreViewTypeToViewType(CoreViewType.FIELDS_WIDGET)).toBe( + ViewType.FieldsWidget, + ); + }); + + it('should fallback to Table for unknown values', () => { + expect(convertCoreViewTypeToViewType('UNKNOWN' as CoreViewType)).toBe( + ViewType.Table, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewFieldGroupToViewFieldGroup.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewFieldGroupToViewFieldGroup.ts new file mode 100644 index 0000000000..ddbc0a6144 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewFieldGroupToViewFieldGroup.ts @@ -0,0 +1,37 @@ +import { type ViewFieldGroup } from '@/views/types/ViewFieldGroup'; +import { convertCoreViewFieldToViewField } from '@/views/utils/convertCoreViewFieldToViewField'; +import { + type CoreViewField, + type CoreViewFieldGroup, +} from '~/generated-metadata/graphql'; + +type CoreViewFieldGroupInput = Pick< + CoreViewFieldGroup, + 'id' | 'name' | 'position' | 'isVisible' | 'viewId' +> & { + viewFields: Pick< + CoreViewField, + | 'id' + | 'fieldMetadataId' + | 'position' + | 'isVisible' + | 'size' + | 'aggregateOperation' + >[]; +}; + +export const convertCoreViewFieldGroupToViewFieldGroup = ( + coreViewFieldGroup: CoreViewFieldGroupInput, +): ViewFieldGroup => { + return { + __typename: 'ViewFieldGroup', + id: coreViewFieldGroup.id, + name: coreViewFieldGroup.name, + position: coreViewFieldGroup.position, + isVisible: coreViewFieldGroup.isVisible, + viewId: coreViewFieldGroup.viewId, + viewFields: coreViewFieldGroup.viewFields.map( + convertCoreViewFieldToViewField, + ), + }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts index c5032f6a03..7b970c66d0 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts @@ -1,5 +1,6 @@ import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; import { type View } from '@/views/types/View'; +import { convertCoreViewFieldGroupToViewFieldGroup } from '@/views/utils/convertCoreViewFieldGroupToViewFieldGroup'; import { convertCoreViewFieldToViewField } from '@/views/utils/convertCoreViewFieldToViewField'; import { convertCoreViewFilterGroupToViewFilterGroup } from '@/views/utils/convertCoreViewFilterGroupToViewFilterGroup'; import { convertCoreViewFilterToViewFilter } from '@/views/utils/convertCoreViewFilterToViewFilter'; @@ -28,6 +29,9 @@ export const convertCoreViewToView = ( viewFields: coreView.viewFields?.map((viewField) => convertCoreViewFieldToViewField(viewField), ), + viewFieldGroups: coreView.viewFieldGroups?.map((viewFieldGroup) => + convertCoreViewFieldGroupToViewFieldGroup(viewFieldGroup), + ), viewGroups: coreView.viewGroups?.map((viewGroup) => convertCoreViewGroupToViewGroup(viewGroup), ), diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts index 0349ca5db4..055b321153 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts @@ -8,6 +8,7 @@ export const convertCoreViewTypeToViewType = ( [CoreViewType.KANBAN]: ViewType.Kanban, [CoreViewType.TABLE]: ViewType.Table, [CoreViewType.CALENDAR]: ViewType.Calendar, + [CoreViewType.FIELDS_WIDGET]: ViewType.FieldsWidget, }; return ( diff --git a/packages/twenty-front/src/testing/mock-data/views.ts b/packages/twenty-front/src/testing/mock-data/views.ts index a5a23374c0..e9ae482130 100644 --- a/packages/twenty-front/src/testing/mock-data/views.ts +++ b/packages/twenty-front/src/testing/mock-data/views.ts @@ -125,6 +125,7 @@ export const mockedCoreViewsData: CoreViewWithRelations[] = [ position: 0, isCompact: false, openRecordIn: CoreViewOpenRecordIn.SIDE_PANEL, + viewFieldGroups: [], viewFilterGroups: [], viewGroups: [], viewFields: [], @@ -147,6 +148,7 @@ export const mockedCoreViewsData: CoreViewWithRelations[] = [ position: 0, isCompact: false, openRecordIn: CoreViewOpenRecordIn.SIDE_PANEL, + viewFieldGroups: [], viewFilterGroups: [], viewGroups: [], viewFields: [], @@ -169,6 +171,7 @@ export const mockedCoreViewsData: CoreViewWithRelations[] = [ position: 0, isCompact: false, openRecordIn: CoreViewOpenRecordIn.SIDE_PANEL, + viewFieldGroups: [], viewFilterGroups: [], viewGroups: [], viewFields: [], @@ -191,6 +194,7 @@ export const mockedCoreViewsData: CoreViewWithRelations[] = [ position: 0, isCompact: false, openRecordIn: CoreViewOpenRecordIn.SIDE_PANEL, + viewFieldGroups: [], viewFilterGroups: [], viewGroups: [], viewFields: [], diff --git a/packages/twenty-front/src/testing/mock-data/widget-configurations.ts b/packages/twenty-front/src/testing/mock-data/widget-configurations.ts index e7b56732f0..533aae601b 100644 --- a/packages/twenty-front/src/testing/mock-data/widget-configurations.ts +++ b/packages/twenty-front/src/testing/mock-data/widget-configurations.ts @@ -9,6 +9,7 @@ import { AggregateOperations, AxisNameDisplay, BarChartLayout, + type FieldsConfiguration, GraphOrderBy, type IframeConfiguration, type StandaloneRichTextConfiguration, @@ -97,10 +98,10 @@ export const TEST_STANDALONE_RICH_TEXT_CONFIGURATION: StandaloneRichTextConfigur body: { markdown: 'Hello world' }, }; -export const TEST_FIELDS_CONFIGURATION = { - __typename: 'FieldsConfiguration' as const, - configurationType: 'FIELDS' as const, - sections: [], +export const TEST_FIELDS_CONFIGURATION: FieldsConfiguration = { + __typename: 'FieldsConfiguration', + configurationType: WidgetConfigurationType.FIELDS, + viewId: null, }; export const ALL_CHART_CONFIGURATIONS: WidgetConfiguration[] = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts index 8a9a403ee4..610a9b023a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts @@ -34,6 +34,8 @@ export const ALL_METADATA_SERIALIZED_RELATION = { pageLayoutTab: {}, pageLayoutWidget: { fieldMetadata: true, + view: true, + viewFieldGroup: true, }, rowLevelPermissionPredicate: {}, rowLevelPermissionPredicateGroup: {}, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts index 22e40e377f..2556259802 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts @@ -8,6 +8,7 @@ import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadat import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout- PageLayoutTabEntity, ObjectMetadataEntity, FieldMetadataEntity, + ViewEntity, ]), ], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts index a684da77f3..e35e949cab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service.ts @@ -17,6 +17,7 @@ import { validateGraphFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata- import { validateIframeFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-creation.util'; import { validateIframeFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-iframe-flat-page-layout-widget-for-update.util'; import { validateSimpleRecordPageWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-simple-record-page-widget-for-creation.util'; +import { validateSimpleRecordPageWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-simple-record-page-widget-for-update.util'; import { validateStandaloneRichTextFlatPageLayoutWidgetForCreation } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-creation.util'; import { validateStandaloneRichTextFlatPageLayoutWidgetForUpdate } from 'src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-standalone-rich-text-flat-page-layout-widget-for-update.util'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; @@ -110,55 +111,35 @@ export class FlatPageLayoutWidgetTypeValidatorService { STANDALONE_RICH_TEXT: validateStandaloneRichTextFlatPageLayoutWidgetForUpdate, FRONT_COMPONENT: validateFrontComponentFlatPageLayoutWidgetForUpdate, - TIMELINE: rejectWidgetType( - WidgetType.TIMELINE, - 'Widget type TIMELINE is not supported yet.', - msg`Widget type TIMELINE is not supported yet.`, + TIMELINE: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.TIMELINE, ), - TASKS: rejectWidgetType( - WidgetType.TASKS, - 'Widget type TASKS is not supported yet.', - msg`Widget type TASKS is not supported yet.`, + TASKS: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.TASKS, ), - NOTES: rejectWidgetType( - WidgetType.NOTES, - 'Widget type NOTES is not supported yet.', - msg`Widget type NOTES is not supported yet.`, + NOTES: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.NOTES, ), - FILES: rejectWidgetType( - WidgetType.FILES, - 'Widget type FILES is not supported yet.', - msg`Widget type FILES is not supported yet.`, + FILES: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.FILES, ), - EMAILS: rejectWidgetType( - WidgetType.EMAILS, - 'Widget type EMAILS is not supported yet.', - msg`Widget type EMAILS is not supported yet.`, + EMAILS: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.EMAILS, ), - CALENDAR: rejectWidgetType( - WidgetType.CALENDAR, - 'Widget type CALENDAR is not supported yet.', - msg`Widget type CALENDAR is not supported yet.`, + CALENDAR: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.CALENDAR, ), - FIELD_RICH_TEXT: rejectWidgetType( - WidgetType.FIELD_RICH_TEXT, - 'Widget type FIELD_RICH_TEXT is not supported yet.', - msg`Widget type FIELD_RICH_TEXT is not supported yet.`, + FIELD_RICH_TEXT: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.FIELD_RICH_TEXT, ), - WORKFLOW: rejectWidgetType( - WidgetType.WORKFLOW, - 'Widget type WORKFLOW is not supported yet.', - msg`Widget type WORKFLOW is not supported yet.`, + WORKFLOW: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.WORKFLOW, ), - WORKFLOW_VERSION: rejectWidgetType( - WidgetType.WORKFLOW_VERSION, - 'Widget type WORKFLOW_VERSION is not supported yet.', - msg`Widget type WORKFLOW_VERSION is not supported yet.`, + WORKFLOW_VERSION: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.WORKFLOW_VERSION, ), - WORKFLOW_RUN: rejectWidgetType( - WidgetType.WORKFLOW_RUN, - 'Widget type WORKFLOW_RUN is not supported yet.', - msg`Widget type WORKFLOW_RUN is not supported yet.`, + WORKFLOW_RUN: validateSimpleRecordPageWidgetForUpdate( + WidgetConfigurationType.WORKFLOW_RUN, ), }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts index eba3d7354a..e6835d4426 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts @@ -13,6 +13,7 @@ import { fromPageLayoutWidgetEntityToFlatPageLayoutWidget } from 'src/engine/met import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -31,6 +32,8 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache private readonly objectMetadataRepository: Repository, @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } @@ -44,6 +47,7 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache pageLayoutTabs, objectMetadatas, fieldMetadatas, + views, ] = await Promise.all([ this.pageLayoutWidgetRepository.find({ where: { workspaceId }, @@ -69,6 +73,11 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache select: ['id', 'universalIdentifier'], withDeleted: true, }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), ]); const applicationIdToUniversalIdentifierMap = @@ -80,6 +89,9 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache const fieldMetadataUniversalIdentifierById = Object.fromEntries( createIdToUniversalIdentifierMap(fieldMetadatas), ); + const viewUniversalIdentifierById = Object.fromEntries( + createIdToUniversalIdentifierMap(views), + ); const flatPageLayoutWidgetMaps = createEmptyFlatEntityMaps(); @@ -91,6 +103,7 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache pageLayoutTabIdToUniversalIdentifierMap, objectMetadataIdToUniversalIdentifierMap, fieldMetadataUniversalIdentifierById, + viewUniversalIdentifierById, }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts index 31ece11a2a..9cdca5db77 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts @@ -20,6 +20,7 @@ export type FromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreateArgs = | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatViewFieldGroupMaps' + | 'flatViewMaps' >; export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ @@ -30,6 +31,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }: FromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreateArgs): FlatPageLayoutWidget => { const { pageLayoutTabId, ...createPageLayoutWidgetInput } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -82,6 +84,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ flatFieldMetadataMaps.universalIdentifierById, viewFieldGroupUniversalIdentifierById: flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: flatViewMaps.universalIdentifierById, shouldThrowOnMissingIdentifier: true, }), }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts index b0c8766e49..93e2005be7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts @@ -74,11 +74,13 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ configuration, fieldMetadataUniversalIdentifierById, viewFieldGroupUniversalIdentifierById = {}, + viewUniversalIdentifierById = {}, shouldThrowOnMissingIdentifier = false, }: { configuration: PageLayoutWidgetConfiguration; fieldMetadataUniversalIdentifierById: Partial>; viewFieldGroupUniversalIdentifierById?: Partial>; + viewUniversalIdentifierById?: Partial>; shouldThrowOnMissingIdentifier?: boolean; }): UniversalPageLayoutWidgetConfiguration => { switch (configuration.configurationType) { @@ -269,10 +271,30 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ } case WidgetConfigurationType.FIELDS: { - const { newFieldDefaultConfiguration, ...rest } = configuration; + const { viewId, newFieldDefaultConfiguration, ...rest } = configuration; + + let viewUniversalIdentifier: string | null = null; + + if (isDefined(viewId)) { + viewUniversalIdentifier = viewUniversalIdentifierById[viewId] ?? null; + + if ( + !isDefined(viewUniversalIdentifier) && + shouldThrowOnMissingIdentifier + ) { + throw new FlatEntityMapsException( + `View universal identifier not found for id: ${viewId}`, + FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND, + ); + } + } if (!isDefined(newFieldDefaultConfiguration)) { - return configuration; + return { + ...rest, + newFieldDefaultConfiguration, + viewId: viewUniversalIdentifier, + }; } let viewFieldGroupUniversalIdentifier: string | null = null; @@ -296,6 +318,7 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ return { ...rest, + viewId: viewUniversalIdentifier, newFieldDefaultConfiguration: { isVisible: newFieldDefaultConfiguration.isVisible, viewFieldGroupId: viewFieldGroupUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts index 0ddb217b56..be0b68a819 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts @@ -13,6 +13,7 @@ type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs = FromEntityToFlatEntityArgs<'pageLayoutWidget'> & { fieldMetadataUniversalIdentifierById: Partial>; viewFieldGroupUniversalIdentifierById?: Partial>; + viewUniversalIdentifierById?: Partial>; }; export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ @@ -22,6 +23,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ objectMetadataIdToUniversalIdentifierMap, fieldMetadataUniversalIdentifierById, viewFieldGroupUniversalIdentifierById, + viewUniversalIdentifierById, }: FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs): FlatPageLayoutWidget => { const pageLayoutWidgetEntityWithoutRelations = removePropertiesFromRecord( pageLayoutWidgetEntity, @@ -73,6 +75,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ configuration: pageLayoutWidgetEntityWithoutRelations.configuration, fieldMetadataUniversalIdentifierById, viewFieldGroupUniversalIdentifierById, + viewUniversalIdentifierById, }); return { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts index 63cd368623..7045ee5481 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts @@ -31,6 +31,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }: { updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId; flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps; @@ -39,6 +40,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatViewFieldGroupMaps' + | 'flatViewMaps' >): FlatPageLayoutWidget => { const { id: pageLayoutWidgetToUpdateId } = extractAndSanitizeObjectStringFields(rawUpdatePageLayoutWidgetInput, [ @@ -102,6 +104,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro flatFieldMetadataMaps.universalIdentifierById, viewFieldGroupUniversalIdentifierById: flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: flatViewMaps.universalIdentifierById, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-simple-record-page-widget-for-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-simple-record-page-widget-for-update.util.ts new file mode 100644 index 0000000000..ae3565330b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/validators/utils/validate-simple-record-page-widget-for-update.util.ts @@ -0,0 +1,39 @@ +import { msg, t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { type ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service'; +import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type'; +import { type WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; +import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception'; + +export const validateSimpleRecordPageWidgetForUpdate = + (expectedConfigurationType: WidgetConfigurationType) => + ( + args: ValidateFlatPageLayoutWidgetTypeSpecificitiesForUpdateArgs, + ): FlatPageLayoutWidgetValidationError[] => { + const { flatEntityToValidate } = args; + const { universalConfiguration, title: widgetTitle } = flatEntityToValidate; + + if (!isDefined(universalConfiguration)) { + return []; + } + + const errors: FlatPageLayoutWidgetValidationError[] = []; + + if ( + universalConfiguration.configurationType !== expectedConfigurationType + ) { + const expectedString = expectedConfigurationType.toString(); + const actualString = + universalConfiguration.configurationType?.toString() ?? 'undefined'; + + errors.push({ + code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + message: t`Invalid configuration type for widget "${widgetTitle}". Expected ${expectedString}, got ${actualString}`, + userFriendlyMessage: msg`Invalid configuration type for widget`, + value: universalConfiguration.configurationType, + }); + } + + return errors; + }; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts index 5fa90bed71..af1c9fb4e4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts @@ -244,6 +244,7 @@ export class PageLayoutWidgetService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -253,6 +254,7 @@ export class PageLayoutWidgetService { 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', 'flatViewFieldGroupMaps', + 'flatViewMaps', ], }, ); @@ -266,6 +268,7 @@ export class PageLayoutWidgetService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }); if (isDefined(createInput.configuration)) { @@ -325,6 +328,7 @@ export class PageLayoutWidgetService { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + flatViewMaps: existingFlatViewMaps, } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -332,6 +336,7 @@ export class PageLayoutWidgetService { 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', 'flatViewFieldGroupMaps', + 'flatViewMaps', ], }, ); @@ -365,6 +370,7 @@ export class PageLayoutWidgetService { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + flatViewMaps: existingFlatViewMaps, }); const shouldValidateChartFields = diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts index 64e94ffe0e..2d8a4ce31f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts @@ -4,10 +4,10 @@ import { isDefined } from 'twenty-shared/utils'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type'; @@ -53,6 +53,7 @@ export class PageLayoutDuplicationService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, } = await this.getPageLayoutFlatEntityMaps(workspaceId); const originalFlatLayout = this.findOriginalLayoutOrThrow( @@ -116,6 +117,7 @@ export class PageLayoutDuplicationService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }); const validateAndBuildResult = @@ -184,6 +186,7 @@ export class PageLayoutDuplicationService { 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', 'flatViewFieldGroupMaps', + 'flatViewMaps', ], }, ); @@ -294,6 +297,7 @@ export class PageLayoutDuplicationService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }: { originalTabsWithWidgets: { tab: FlatPageLayoutTab; @@ -306,6 +310,7 @@ export class PageLayoutDuplicationService { flatObjectMetadataMaps: AllFlatEntityMaps['flatObjectMetadataMaps']; flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps']; flatViewFieldGroupMaps: AllFlatEntityMaps['flatViewFieldGroupMaps']; + flatViewMaps: AllFlatEntityMaps['flatViewMaps']; }): FlatPageLayoutWidget[] { return originalTabsWithWidgets.flatMap(({ tab, widgets }) => { const newTabId = originalTabIdToNewTabIdMap.get(tab.id)!; @@ -326,6 +331,7 @@ export class PageLayoutDuplicationService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }), ); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts index 022492832d..1498e06cc6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts @@ -120,6 +120,7 @@ export class PageLayoutUpdateService { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -128,6 +129,7 @@ export class PageLayoutUpdateService { 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', 'flatViewFieldGroupMaps', + 'flatViewMaps', ], }, ); @@ -153,6 +155,7 @@ export class PageLayoutUpdateService { workspaceCustomFlatApplication.universalIdentifier, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }); const validateAndBuildResult = @@ -365,6 +368,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }: { tabs: UpdatePageLayoutTabWithWidgetsInput[]; workspaceId: string; @@ -375,6 +379,7 @@ export class PageLayoutUpdateService { | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatViewFieldGroupMaps' + | 'flatViewMaps' | 'flatPageLayoutTabMaps' | 'flatPageLayoutWidgetMaps' >): { @@ -398,6 +403,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }); allWidgetsToCreate.push(...widgetsToCreate); @@ -422,6 +428,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, flatViewFieldGroupMaps, + flatViewMaps, }: { tabId: string; widgets: UpdatePageLayoutWidgetWithIdInput[]; @@ -433,6 +440,7 @@ export class PageLayoutUpdateService { | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatViewFieldGroupMaps' + | 'flatViewMaps' | 'flatPageLayoutTabMaps' | 'flatPageLayoutWidgetMaps' >): { @@ -503,6 +511,7 @@ export class PageLayoutUpdateService { flatFieldMetadataMaps.universalIdentifierById, viewFieldGroupUniversalIdentifierById: flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: flatViewMaps.universalIdentifierById, }), }; }, @@ -549,6 +558,8 @@ export class PageLayoutUpdateService { flatFieldMetadataMaps.universalIdentifierById, viewFieldGroupUniversalIdentifierById: flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: + flatViewMaps.universalIdentifierById, }), }), }; @@ -597,6 +608,8 @@ export class PageLayoutUpdateService { flatFieldMetadataMaps.universalIdentifierById, viewFieldGroupUniversalIdentifierById: flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: + flatViewMaps.universalIdentifierById, }), }), }; diff --git a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts index caba306bea..c5dff90252 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts @@ -9,8 +9,8 @@ import { } from '@nestjs/graphql'; import { isArray } from '@sniptt/guards'; +import { ViewType, ViewVisibility } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { ViewVisibility } from 'twenty-shared/types'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -112,16 +112,23 @@ export class ViewResolver { @AuthUserWorkspaceId() userWorkspaceId: string | undefined, @Args('objectMetadataId', { type: () => String, nullable: true }) objectMetadataId?: string, + @Args('viewTypes', { type: () => [ViewType], nullable: true }) + viewTypes?: ViewType[], ): Promise { if (objectMetadataId) { return this.viewService.findByObjectMetadataId( workspace.id, objectMetadataId, userWorkspaceId, + viewTypes, ); } - return this.viewService.findByWorkspaceId(workspace.id, userWorkspaceId); + return this.viewService.findByWorkspaceId( + workspace.id, + userWorkspaceId, + viewTypes, + ); } @Query(() => ViewDTO, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts index ae2d4e7efd..9261b201b8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { ViewType, ViewVisibility } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Repository } from 'typeorm'; -import { ViewVisibility } from 'twenty-shared/types'; +import { In, IsNull, Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -375,11 +375,13 @@ export class ViewService { async findByWorkspaceId( workspaceId: string, userWorkspaceId?: string, + viewTypes?: ViewType[], ): Promise { const views = await this.viewRepository.find({ where: { workspaceId, deletedAt: IsNull(), + ...(viewTypes && viewTypes.length > 0 && { type: In(viewTypes) }), }, order: { position: 'ASC' }, relations: [ @@ -412,12 +414,14 @@ export class ViewService { workspaceId: string, objectMetadataId: string, userWorkspaceId?: string, + viewTypes?: ViewType[], ): Promise { const views = await this.viewRepository.find({ where: { workspaceId, objectMetadataId, deletedAt: IsNull(), + ...(viewTypes && viewTypes.length > 0 && { type: In(viewTypes) }), }, order: { position: 'ASC' }, relations: [ diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap index 7cbaff1e69..76ee6c350c 100644 --- a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap @@ -3,15 +3,19 @@ exports[`Page layout with tabs creation via update should succeed should create a new tab with a widget via page layout update 1`] = ` { "createdAt": Any, + "defaultTabToFocusOnMobileAndSidePanelId": null, "deletedAt": null, "id": Any, "name": "Layout With New Tab", "objectMetadataId": null, "tabs": [ { + "applicationId": Any, "createdAt": Any, "deletedAt": null, + "icon": null, "id": Any, + "layoutMode": "GRID", "pageLayoutId": Any, "position": 0, "title": "Existing Tab", @@ -54,9 +58,12 @@ exports[`Page layout with tabs creation via update should succeed should create ], }, { + "applicationId": Any, "createdAt": Any, "deletedAt": null, + "icon": null, "id": Any, + "layoutMode": "GRID", "pageLayoutId": Any, "position": 1, "title": "New Tab Created Via Update", diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap index c2a69f0e5f..a81d341f49 100644 --- a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap @@ -3,15 +3,19 @@ exports[`Page layout with tabs update should succeed should update page layout with multiple tabs and different widget types 1`] = ` { "createdAt": Any, + "defaultTabToFocusOnMobileAndSidePanelId": null, "deletedAt": null, "id": Any, "name": "Multi-Tab Layout", "objectMetadataId": null, "tabs": [ { + "applicationId": Any, "createdAt": Any, "deletedAt": null, + "icon": null, "id": Any, + "layoutMode": "GRID", "pageLayoutId": Any, "position": 0, "title": "First Tab", @@ -54,9 +58,12 @@ exports[`Page layout with tabs update should succeed should update page layout w ], }, { + "applicationId": Any, "createdAt": Any, "deletedAt": null, + "icon": null, "id": Any, + "layoutMode": "GRID", "pageLayoutId": Any, "position": 1, "title": "Second Tab", @@ -93,15 +100,19 @@ exports[`Page layout with tabs update should succeed should update page layout w exports[`Page layout with tabs update should succeed should update page layout with single tab and widget 1`] = ` { "createdAt": Any, + "defaultTabToFocusOnMobileAndSidePanelId": null, "deletedAt": null, "id": Any, "name": "Updated Page Layout", "objectMetadataId": null, "tabs": [ { + "applicationId": Any, "createdAt": Any, "deletedAt": null, + "icon": null, "id": Any, + "layoutMode": "GRID", "pageLayoutId": Any, "position": 0, "title": "Updated Tab 1", diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts index ec2d8b636a..5256c6bcb6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts @@ -13,13 +13,17 @@ const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` name type objectMetadataId + defaultTabToFocusOnMobileAndSidePanelId createdAt updatedAt deletedAt tabs { id + applicationId title + icon position + layoutMode pageLayoutId createdAt updatedAt