From e9aa6f47e5381268bb1189403ec9c6b4ab343276 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 23 Mar 2026 00:44:49 +0100 Subject: [PATCH] Allow users to create Fields and Field widget (#18801) - Allow users to create a Fields widget or a Field widget; **this PR is focused on Fields widgets as Field widget can't be configured yet** - Automatically create a filled view when the user creates a draft Fields widget in edit mode https://github.com/user-attachments/assets/b2dbba52-c614-44cd-bf6c-095ce9d4ec26 --------- Co-authored-by: Charles Bochet --- .../components/PageLayoutContent.tsx | 16 +- .../PageLayoutVerticalListEditor.tsx | 9 +- .../hooks/useCreateRecordPageFieldWidget.ts | 86 ++ .../hooks/useCreateRecordPageFieldsWidget.ts | 101 ++ .../hooks/useNavigateToMoreWidgets.ts | 15 + .../utils/createDefaultFieldWidget.ts | 51 + .../utils/createDefaultFieldsWidget.ts | 50 + .../components/RecordPageAddWidgetSection.tsx | 90 ++ .../widgets/components/WidgetRenderer.tsx | 22 +- .../clients/generated/metadata/schema.graphql | 300 ++--- .../src/clients/generated/metadata/schema.ts | 188 +-- .../src/clients/generated/metadata/types.ts | 1010 ++++++++--------- .../page-layout/page-layout.module.ts | 2 + .../services/page-layout-update.service.ts | 122 +- .../view/services/view.service.ts | 50 +- ...w-fields-and-groups-to-create.util.spec.ts | 415 +++++++ ...t-view-fields-and-groups-to-create.util.ts | 137 +++ .../display/icon/components/TablerIcons.ts | 14 +- packages/twenty-ui/src/display/index.ts | 14 +- 19 files changed, 1921 insertions(+), 771 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldsWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx create mode 100644 packages/twenty-server/src/engine/metadata-modules/view/utils/__tests__/compute-fields-widget-view-fields-and-groups-to-create.util.spec.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx index 0c8cd86dd9..fb619aeb49 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx @@ -7,7 +7,10 @@ import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageL import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow'; import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets'; +import { RecordPageAddWidgetSection } from '@/page-layout/widgets/components/RecordPageAddWidgetSection'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { + FeatureFlagKey, PageLayoutTabLayoutMode, PageLayoutType, } from '~/generated-metadata/graphql'; @@ -28,6 +31,10 @@ export const PageLayoutContent = () => { const isRecordPageLayout = currentPageLayout.type === PageLayoutType.RECORD_PAGE; + const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED, + ); + const isCanvasLayout = layoutMode === PageLayoutTabLayoutMode.CANVAS; const isVerticalList = layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST; @@ -36,12 +43,19 @@ export const PageLayoutContent = () => { } if (isVerticalList) { - if (!isRecordPageLayout && isPageLayoutInEditMode) { + if ( + isPageLayoutInEditMode && + isRecordPageLayout && + isRecordPageGlobalEditionEnabled + ) { return ( : undefined + } /> ); } diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx index 08cb13339e..61599bcdac 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx @@ -6,16 +6,16 @@ import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer' import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; -import { styled } from '@linaria/react'; import { DragDropContext, Draggable, Droppable, type DropResult, } from '@hello-pangea/dnd'; -import { useId } from 'react'; -import { useIsMobile } from 'twenty-ui/utilities'; +import { styled } from '@linaria/react'; +import { type ReactNode, useId } from 'react'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { useIsMobile } from 'twenty-ui/utilities'; const StyledVerticalListContainer = styled.div<{ variant: PageLayoutVerticalListViewerVariant; @@ -47,12 +47,14 @@ type PageLayoutVerticalListEditorProps = { widgets: PageLayoutWidget[]; onReorder: (result: DropResult) => void; isReorderEnabled?: boolean; + trailingElement?: ReactNode; }; export const PageLayoutVerticalListEditor = ({ widgets, onReorder, isReorderEnabled = true, + trailingElement, }: PageLayoutVerticalListEditorProps) => { const droppableId = `page-layout-vertical-list-${useId()}`; @@ -112,6 +114,7 @@ export const PageLayoutVerticalListEditor = ({ ))} {provided.placeholder} + {trailingElement} )} diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts new file mode 100644 index 0000000000..bac81cdd3f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts @@ -0,0 +1,86 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; +import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; +import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget'; +import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { WidgetConfigurationType } from '~/generated-metadata/graphql'; + +export const useCreateRecordPageFieldWidget = () => { + const { tabId } = usePageLayoutContentContext(); + + const { targetObjectNameSingular } = useTargetRecord(); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: targetObjectNameSingular, + }); + + const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({ + objectNameSingular: targetObjectNameSingular, + }); + + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + + const pageLayoutDraftState = useAtomComponentStateCallbackState( + pageLayoutDraftComponentState, + ); + + const store = useStore(); + + const createRecordPageFieldWidget = useCallback(() => { + const activeTab = currentPageLayout.tabs.find((tab) => tab.id === tabId); + const existingWidgets = activeTab?.widgets ?? []; + + const usedFieldMetadataIds = new Set( + existingWidgets + .filter( + (widget) => + widget.configuration.configurationType === + WidgetConfigurationType.FIELD, + ) + .map((widget) => { + const configuration = widget.configuration as { + fieldMetadataId: string; + }; + return configuration.fieldMetadataId; + }), + ); + + const availableRelationField = boxedRelationFieldMetadataItems.find( + (field) => !usedFieldMetadataIds.has(field.id), + ); + + const fieldMetadataId = availableRelationField?.id ?? ''; + + const positionIndex = existingWidgets.length; + const widgetId = uuidv4(); + + const newWidget = createDefaultFieldWidget({ + id: widgetId, + pageLayoutTabId: tabId, + fieldMetadataId, + objectMetadataId: objectMetadataItem.id, + positionIndex, + }); + + store.set(pageLayoutDraftState, (prev) => ({ + ...prev, + tabs: addWidgetToTab(prev.tabs, tabId, newWidget), + })); + }, [ + boxedRelationFieldMetadataItems, + currentPageLayout.tabs, + objectMetadataItem.id, + pageLayoutDraftState, + store, + tabId, + ]); + + return { createRecordPageFieldWidget }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts new file mode 100644 index 0000000000..a7130ea07c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts @@ -0,0 +1,101 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; +import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget'; +import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; +import { v4 as uuidv4 } from 'uuid'; +import { ViewType } from '~/generated-metadata/graphql'; + +export const useCreateRecordPageFieldsWidget = () => { + const { tabId } = usePageLayoutContentContext(); + + const { targetObjectNameSingular } = useTargetRecord(); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: targetObjectNameSingular, + }); + + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + + const { performViewAPICreate } = usePerformViewAPIPersist(); + + const pageLayoutDraftState = useAtomComponentStateCallbackState( + pageLayoutDraftComponentState, + ); + + const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState( + pageLayoutEditingWidgetIdComponentState, + ); + + const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + + const store = useStore(); + + const createRecordPageFieldsWidget = useCallback(async () => { + const viewId = uuidv4(); + + const result = await performViewAPICreate( + { + input: { + id: viewId, + name: `${objectMetadataItem.labelSingular} Fields`, + icon: 'IconList', + objectMetadataId: objectMetadataItem.id, + type: ViewType.FIELDS_WIDGET, + }, + }, + objectMetadataItem.id, + ); + + if (result.status === 'failed') { + return; + } + + const activeTab = currentPageLayout.tabs.find((tab) => tab.id === tabId); + const positionIndex = activeTab?.widgets.length ?? 0; + + const widgetId = uuidv4(); + + const newWidget = createDefaultFieldsWidget({ + id: widgetId, + pageLayoutTabId: tabId, + viewId, + objectMetadataId: objectMetadataItem.id, + positionIndex, + }); + + store.set(pageLayoutDraftState, (prev) => ({ + ...prev, + tabs: addWidgetToTab(prev.tabs, tabId, newWidget), + })); + + store.set(pageLayoutEditingWidgetIdState, widgetId); + + navigatePageLayoutSidePanel({ + sidePanelPage: SidePanelPages.PageLayoutFieldsSettings, + focusTitleInput: true, + resetNavigationStack: true, + }); + }, [ + currentPageLayout.tabs, + navigatePageLayoutSidePanel, + objectMetadataItem.id, + objectMetadataItem.labelSingular, + pageLayoutDraftState, + pageLayoutEditingWidgetIdState, + performViewAPICreate, + store, + tabId, + ]); + + return { createRecordPageFieldsWidget }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts new file mode 100644 index 0000000000..277ea99d5d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts @@ -0,0 +1,15 @@ +import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { useCallback } from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; + +export const useNavigateToMoreWidgets = () => { + const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + + const navigateToMoreWidgets = useCallback(() => { + navigatePageLayoutSidePanel({ + sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect, + }); + }, [navigatePageLayoutSidePanel]); + + return { navigateToMoreWidgets }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts new file mode 100644 index 0000000000..5494a0a59c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts @@ -0,0 +1,51 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { + PageLayoutTabLayoutMode, + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +export const createDefaultFieldWidget = ({ + id, + pageLayoutTabId, + fieldMetadataId, + objectMetadataId, + positionIndex, +}: { + id: string; + pageLayoutTabId: string; + fieldMetadataId: string; + objectMetadataId: string; + positionIndex: number; +}): PageLayoutWidget => { + return { + __typename: 'PageLayoutWidget', + id, + pageLayoutTabId, + title: '', + type: WidgetType.FIELD, + configuration: { + __typename: 'FieldConfiguration', + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId, + layout: 'CARD', + }, + gridPosition: { + __typename: 'GridPosition', + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 12, + }, + position: { + __typename: 'PageLayoutWidgetVerticalListPosition', + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + index: positionIndex, + }, + objectMetadataId: objectMetadataId ?? null, + isOverridden: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldsWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldsWidget.ts new file mode 100644 index 0000000000..b28c7eb943 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldsWidget.ts @@ -0,0 +1,50 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { + PageLayoutTabLayoutMode, + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +export const createDefaultFieldsWidget = ({ + id, + pageLayoutTabId, + viewId, + objectMetadataId, + positionIndex, +}: { + id: string; + pageLayoutTabId: string; + viewId: string; + objectMetadataId: string; + positionIndex: number; +}): PageLayoutWidget => { + return { + __typename: 'PageLayoutWidget', + id, + pageLayoutTabId, + title: 'Fields', + type: WidgetType.FIELDS, + configuration: { + __typename: 'FieldsConfiguration', + configurationType: WidgetConfigurationType.FIELDS, + viewId, + }, + gridPosition: { + __typename: 'GridPosition', + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 12, + }, + position: { + __typename: 'PageLayoutWidgetVerticalListPosition', + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + index: positionIndex, + }, + objectMetadataId: objectMetadataId ?? null, + isOverridden: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx new file mode 100644 index 0000000000..b4e8a8e50f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx @@ -0,0 +1,90 @@ +import { useCreateRecordPageFieldWidget } from '@/page-layout/hooks/useCreateRecordPageFieldWidget'; +import { useCreateRecordPageFieldsWidget } from '@/page-layout/hooks/useCreateRecordPageFieldsWidget'; +import { useNavigateToMoreWidgets } from '@/page-layout/hooks/useNavigateToMoreWidgets'; +import { styled } from '@linaria/react'; +import { t } from '@lingui/core/macro'; +import { useContext } from 'react'; +import { + IconListDetails, + IconListSearch, + IconPlus, + IconSquarePlus, +} from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledContainer = styled.div` + border: 1px solid transparent; + border-radius: ${themeCssVariables.border.radius.md}; + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: ${themeCssVariables.spacing[2]}; + width: 100%; +`; + +const StyledHeader = styled.div` + align-items: center; + color: ${themeCssVariables.font.color.primary}; + display: flex; + flex-shrink: 0; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.medium}; + gap: ${themeCssVariables.spacing[1]}; + height: ${themeCssVariables.spacing[6]}; + padding-inline: ${themeCssVariables.spacing[1]}; +`; + +const StyledMenuItemList = styled.div` + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.md}; + margin-top: ${themeCssVariables.spacing[2]}; + overflow: hidden; + padding: ${themeCssVariables.spacing[2]}; +`; + +export const RecordPageAddWidgetSection = () => { + const { theme } = useContext(ThemeContext); + + const { createRecordPageFieldsWidget } = useCreateRecordPageFieldsWidget(); + + const { createRecordPageFieldWidget } = useCreateRecordPageFieldWidget(); + + const { navigateToMoreWidgets } = useNavigateToMoreWidgets(); + + return ( + + + + {t`Add widget`} + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index f0888cbfab..0ec62dcefd 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -24,12 +24,14 @@ import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingC import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { styled } from '@linaria/react'; import { type MouseEvent, useContext } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { IconLock } from 'twenty-ui/display'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { + FeatureFlagKey, PageLayoutTabLayoutMode, PageLayoutType, WidgetType, @@ -83,17 +85,27 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED, + ); + + const isRecordPageLayout = + currentPageLayout.type === PageLayoutType.RECORD_PAGE; + const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id); const isReorderEnabled = - currentPageLayout.type !== PageLayoutType.RECORD_PAGE; + !isRecordPageLayout || + (isRecordPageLayout && isRecordPageGlobalEditionEnabled); const isDeletingWidgetEnabled = - currentPageLayout.type !== PageLayoutType.RECORD_PAGE; + !isRecordPageLayout || + (isRecordPageLayout && isRecordPageGlobalEditionEnabled); const isWidgetEditable = isPageLayoutInEditMode && - (currentPageLayout.type !== PageLayoutType.RECORD_PAGE || + (!isRecordPageLayout || + (isRecordPageLayout && isRecordPageGlobalEditionEnabled) || widget.type === WidgetType.FIELDS); // TODO: when we have more widgets without headers, we should use a more generic approach to hide the header @@ -141,7 +153,9 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { // TODO: remove once all record page layouts widgets use the editable contain in edit mode const shouldWrapWithEditingWrapper = - isWidgetEditable && variant === 'side-column'; + isWidgetEditable && + variant === 'side-column' && + !isRecordPageGlobalEditionEnabled; const widgetCard = ( ['flatPageLayoutWidgetMaps']; + }): string[] { + const viewIdsToDelete = new Set(); + const directlyDeletedWidgetIds = new Set(); + + // Collect viewIds from directly deleted FIELDS widgets + for (const widget of widgetsToUpdate) { + if (isDefined(widget.deletedAt)) { + directlyDeletedWidgetIds.add(widget.id); + const viewId = this.getViewIdFromFieldsWidget(widget); + + if (isDefined(viewId)) { + viewIdsToDelete.add(viewId); + } + } + } + + // Collect viewIds from FIELDS widgets in deleted tabs + const deletedTabIds = new Set( + tabsToUpdate + .filter((tab) => isDefined(tab.deletedAt)) + .map((tab) => tab.id), + ); + + const allExistingWidgets = Object.values( + flatPageLayoutWidgetMaps.byUniversalIdentifier, + ).filter(isDefined); + + for (const widget of allExistingWidgets) { + if ( + !isDefined(widget.deletedAt) && + deletedTabIds.has(widget.pageLayoutTabId) + ) { + const viewId = this.getViewIdFromFieldsWidget(widget); + + if (isDefined(viewId)) { + viewIdsToDelete.add(viewId); + } + } + } + + // Filter out viewIds still referenced by surviving widgets + for (const widget of allExistingWidgets) { + if ( + !isDefined(widget.deletedAt) && + !directlyDeletedWidgetIds.has(widget.id) && + !deletedTabIds.has(widget.pageLayoutTabId) + ) { + const viewId = this.getViewIdFromFieldsWidget(widget); + + if (isDefined(viewId)) { + viewIdsToDelete.delete(viewId); + } + } + } + + return [...viewIdsToDelete]; + } + + private getViewIdFromFieldsWidget( + widget: FlatPageLayoutWidget, + ): string | undefined { + if ( + widget.configuration.configurationType !== WidgetConfigurationType.FIELDS + ) { + return undefined; + } + + const viewId = (widget.configuration as { viewId?: string | null }).viewId; + + return typeof viewId === 'string' ? viewId : undefined; + } + + private async destroyOrphanedFieldsWidgetViews({ + viewIds, + workspaceId, + }: { + viewIds: string[]; + workspaceId: string; + }): Promise { + for (const viewId of viewIds) { + try { + await this.viewService.destroyOne({ + destroyViewInput: { id: viewId }, + workspaceId, + }); + } catch (error) { + this.logger.warn( + `Failed to destroy view ${viewId} after Fields widget deletion: ${error}`, + ); + } + } + } } 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 f9396588ce..b3f530990a 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 @@ -10,8 +10,8 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.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 { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; @@ -31,9 +31,12 @@ import { DestroyViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/d import { UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input'; import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { computeFieldsWidgetViewFieldsAndGroupsToCreate } from 'src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util'; import { fromFlatViewToViewDto } from 'src/engine/metadata-modules/view/utils/from-flat-view-to-view-dto.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; +import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; @Injectable() export class ViewService { @@ -82,6 +85,39 @@ export class ViewService { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); + let flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[] = []; + let flatViewFieldsToCreate: UniversalFlatViewField[] = []; + + if (flatViewToCreate.type === ViewType.FIELDS_WIDGET) { + const objectFlatFieldMetadatas = Object.values( + existingFlatFieldMetadataMaps.byUniversalIdentifier, + ).filter( + (field): field is NonNullable => + field !== undefined && + field.objectMetadataUniversalIdentifier === + flatViewToCreate.objectMetadataUniversalIdentifier, + ); + + const objectFlatMetadata = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: existingFlatObjectMetadataMaps, + universalIdentifier: flatViewToCreate.objectMetadataUniversalIdentifier, + }); + + const fieldsWidgetResult = computeFieldsWidgetViewFieldsAndGroupsToCreate( + { + objectFlatFieldMetadatas, + viewUniversalIdentifier: flatViewToCreate.universalIdentifier, + flatApplication: workspaceCustomFlatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: + objectFlatMetadata.labelIdentifierFieldMetadataUniversalIdentifier, + }, + ); + + flatViewFieldGroupsToCreate = + fieldsWidgetResult.flatViewFieldGroupsToCreate; + flatViewFieldsToCreate = fieldsWidgetResult.flatViewFieldsToCreate; + } + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -97,6 +133,18 @@ export class ViewService { flatEntityToDelete: [], flatEntityToUpdate: [], }, + + viewFieldGroup: { + flatEntityToCreate: flatViewFieldGroupsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + + viewField: { + flatEntityToCreate: flatViewFieldsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, }, workspaceId, isSystemBuild: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/__tests__/compute-fields-widget-view-fields-and-groups-to-create.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/__tests__/compute-fields-widget-view-fields-and-groups-to-create.util.spec.ts new file mode 100644 index 0000000000..1d7554ae31 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/__tests__/compute-fields-widget-view-fields-and-groups-to-create.util.spec.ts @@ -0,0 +1,415 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; + +import { computeFieldsWidgetViewFieldsAndGroupsToCreate } from '../compute-fields-widget-view-fields-and-groups-to-create.util'; + +const makeFieldMetadata = ( + overrides: Partial & { + name: string; + type: FieldMetadataType; + }, +): UniversalFlatFieldMetadata => { + const universalIdentifier = overrides.universalIdentifier ?? v4(); + + return { + universalIdentifier, + objectMetadataUniversalIdentifier: 'object-uid', + applicationUniversalIdentifier: 'app-uid', + name: overrides.name, + label: overrides.label ?? overrides.name, + type: overrides.type, + isCustom: overrides.isCustom ?? false, + isActive: true, + isSystem: false, + isUIReadOnly: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: false, + defaultValue: null, + description: null, + icon: null, + options: null, + settings: null, + standardOverrides: null, + relationTargetObjectMetadataUniversalIdentifier: null, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + deletedAt: null, + } as unknown as UniversalFlatFieldMetadata; +}; + +const flatApplication = { + universalIdentifier: 'app-uid', + id: 'app-id', +} as never; + +const viewUniversalIdentifier = 'view-uid'; + +describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => { + it('should create a "General" group with standard fields', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + isCustom: false, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(result.flatViewFieldGroupsToCreate).toHaveLength(1); + expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General'); + expect(result.flatViewFieldGroupsToCreate[0].position).toBe(0); + expect(result.flatViewFieldGroupsToCreate[0].isVisible).toBe(true); + + expect(result.flatViewFieldsToCreate).toHaveLength(2); + result.flatViewFieldsToCreate.forEach((vf) => { + expect(vf.viewFieldGroupUniversalIdentifier).toBe( + result.flatViewFieldGroupsToCreate[0].universalIdentifier, + ); + expect(vf.isVisible).toBe(true); + expect(vf.size).toBe(DEFAULT_VIEW_FIELD_SIZE); + expect(vf.viewUniversalIdentifier).toBe(viewUniversalIdentifier); + }); + + expect(result.flatViewFieldsToCreate[0].position).toBe(0); + expect(result.flatViewFieldsToCreate[1].position).toBe(1); + }); + + it('should create an "Other" group when custom fields exist', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'customRating', + type: FieldMetadataType.NUMBER, + isCustom: true, + }), + makeFieldMetadata({ + name: 'customTag', + type: FieldMetadataType.TEXT, + isCustom: true, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(result.flatViewFieldGroupsToCreate).toHaveLength(2); + expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General'); + expect(result.flatViewFieldGroupsToCreate[0].position).toBe(0); + expect(result.flatViewFieldGroupsToCreate[1].name).toBe('Other'); + expect(result.flatViewFieldGroupsToCreate[1].position).toBe(1); + + const generalGroupUid = + result.flatViewFieldGroupsToCreate[0].universalIdentifier; + const otherGroupUid = + result.flatViewFieldGroupsToCreate[1].universalIdentifier; + + const generalFields = result.flatViewFieldsToCreate.filter( + (vf) => vf.viewFieldGroupUniversalIdentifier === generalGroupUid, + ); + const otherFields = result.flatViewFieldsToCreate.filter( + (vf) => vf.viewFieldGroupUniversalIdentifier === otherGroupUid, + ); + + expect(generalFields).toHaveLength(1); + expect(otherFields).toHaveLength(2); + + // Positions are sequential within each group + expect(otherFields[0].position).toBe(0); + expect(otherFields[1].position).toBe(1); + }); + + it('should not create an "Other" group when there are no custom fields', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(result.flatViewFieldGroupsToCreate).toHaveLength(1); + expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General'); + }); + + it('should exclude deletedAt field', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'deletedAt', + type: FieldMetadataType.DATE_TIME, + isCustom: false, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(result.flatViewFieldsToCreate).toHaveLength(1); + expect( + result.flatViewFieldsToCreate[0].fieldMetadataUniversalIdentifier, + ).toBe(fields[0].universalIdentifier); + }); + + it('should exclude TS_VECTOR and POSITION fields', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + isCustom: false, + }), + makeFieldMetadata({ + name: 'position', + type: FieldMetadataType.POSITION, + isCustom: false, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(result.flatViewFieldsToCreate).toHaveLength(1); + }); + + it('should exclude id field unless it is the label identifier', () => { + const idUid = v4(); + + const fields = [ + makeFieldMetadata({ + name: 'id', + type: FieldMetadataType.UUID, + isCustom: false, + universalIdentifier: idUid, + }), + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + ]; + + // Without label identifier match: id excluded + const resultWithout = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + expect(resultWithout.flatViewFieldsToCreate).toHaveLength(1); + + // With label identifier match: id included + const resultWith = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: idUid, + }); + + expect(resultWith.flatViewFieldsToCreate).toHaveLength(2); + }); + + it('should set correct applicationUniversalIdentifier on all entities', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'custom', + type: FieldMetadataType.TEXT, + isCustom: true, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + result.flatViewFieldGroupsToCreate.forEach((group) => { + expect(group.applicationUniversalIdentifier).toBe('app-uid'); + }); + result.flatViewFieldsToCreate.forEach((field) => { + expect(field.applicationUniversalIdentifier).toBe('app-uid'); + }); + }); + + it('should return empty fields when all fields are excluded', () => { + const fields = [ + makeFieldMetadata({ + name: 'deletedAt', + type: FieldMetadataType.DATE_TIME, + isCustom: false, + }), + makeFieldMetadata({ + name: 'searchVector', + type: FieldMetadataType.TS_VECTOR, + isCustom: false, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + // Still creates the "General" group, just with no fields + expect(result.flatViewFieldGroupsToCreate).toHaveLength(1); + expect(result.flatViewFieldsToCreate).toHaveLength(0); + }); + + it('should place label identifier field at position 0', () => { + const labelUid = v4(); + + const fields = [ + makeFieldMetadata({ + name: 'createdAt', + type: FieldMetadataType.DATE_TIME, + isCustom: false, + }), + makeFieldMetadata({ + name: 'updatedAt', + type: FieldMetadataType.DATE_TIME, + isCustom: false, + }), + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + universalIdentifier: labelUid, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: labelUid, + }); + + expect(result.flatViewFieldsToCreate).toHaveLength(3); + + // The label identifier field must be at the lowest position + const labelField = result.flatViewFieldsToCreate.find( + (vf) => vf.fieldMetadataUniversalIdentifier === labelUid, + ); + + expect(labelField).toBeDefined(); + expect(labelField!.position).toBe(0); + + // Other fields should have higher positions + const otherPositions = result.flatViewFieldsToCreate + .filter((vf) => vf.fieldMetadataUniversalIdentifier !== labelUid) + .map((vf) => vf.position); + + otherPositions.forEach((pos) => { + expect(pos).toBeGreaterThan(0); + }); + }); + + it('should make RELATION and MORPH_RELATION fields invisible by default', () => { + const fields = [ + makeFieldMetadata({ + name: 'name', + type: FieldMetadataType.TEXT, + isCustom: false, + }), + makeFieldMetadata({ + name: 'company', + type: FieldMetadataType.RELATION, + isCustom: false, + }), + makeFieldMetadata({ + name: 'target', + type: FieldMetadataType.MORPH_RELATION, + isCustom: false, + }), + makeFieldMetadata({ + name: 'customRelation', + type: FieldMetadataType.RELATION, + isCustom: true, + }), + ]; + + const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({ + objectFlatFieldMetadatas: fields, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: null, + }); + + const viewFieldForName = result.flatViewFieldsToCreate.find( + (vf) => + vf.fieldMetadataUniversalIdentifier === fields[0].universalIdentifier, + ); + const viewFieldForCompany = result.flatViewFieldsToCreate.find( + (vf) => + vf.fieldMetadataUniversalIdentifier === fields[1].universalIdentifier, + ); + const viewFieldForTarget = result.flatViewFieldsToCreate.find( + (vf) => + vf.fieldMetadataUniversalIdentifier === fields[2].universalIdentifier, + ); + const viewFieldForCustomRelation = result.flatViewFieldsToCreate.find( + (vf) => + vf.fieldMetadataUniversalIdentifier === fields[3].universalIdentifier, + ); + + expect(viewFieldForName!.isVisible).toBe(true); + expect(viewFieldForCompany!.isVisible).toBe(false); + expect(viewFieldForTarget!.isVisible).toBe(false); + expect(viewFieldForCustomRelation!.isVisible).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts new file mode 100644 index 0000000000..2c0c377481 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts @@ -0,0 +1,137 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; +import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; + +export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({ + objectFlatFieldMetadatas, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier, +}: { + objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; + viewUniversalIdentifier: string; + flatApplication: FlatApplication; + labelIdentifierFieldMetadataUniversalIdentifier: string | null; +}): { + flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[]; + flatViewFieldsToCreate: UniversalFlatViewField[]; +} => { + const createdAt = new Date().toISOString(); + const applicationUniversalIdentifier = flatApplication.universalIdentifier; + + const eligibleFields = objectFlatFieldMetadatas.filter( + (field) => + field.name !== 'deletedAt' && + field.type !== FieldMetadataType.TS_VECTOR && + field.type !== FieldMetadataType.POSITION && + (field.name !== 'id' || + field.universalIdentifier === + labelIdentifierFieldMetadataUniversalIdentifier), + ); + + const standardFields = eligibleFields.filter((field) => !field.isCustom); + const customFields = eligibleFields.filter((field) => field.isCustom); + + const sortedStandardFields = [...standardFields].sort((a, b) => { + const aIsLabel = + a.universalIdentifier === labelIdentifierFieldMetadataUniversalIdentifier; + const bIsLabel = + b.universalIdentifier === labelIdentifierFieldMetadataUniversalIdentifier; + + if (aIsLabel && !bIsLabel) return -1; + if (!aIsLabel && bIsLabel) return 1; + + return 0; + }); + + const flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[] = []; + const flatViewFieldsToCreate: UniversalFlatViewField[] = []; + + const generalGroupUniversalIdentifier = v4(); + + flatViewFieldGroupsToCreate.push({ + universalIdentifier: generalGroupUniversalIdentifier, + applicationUniversalIdentifier, + viewUniversalIdentifier, + name: 'General', + position: 0, + isVisible: true, + overrides: null, + viewFieldUniversalIdentifiers: [], + createdAt, + updatedAt: createdAt, + deletedAt: null, + }); + + sortedStandardFields.forEach((field, index) => { + const isVisible = + field.type !== FieldMetadataType.RELATION && + field.type !== FieldMetadataType.MORPH_RELATION; + + flatViewFieldsToCreate.push({ + fieldMetadataUniversalIdentifier: field.universalIdentifier, + viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier: generalGroupUniversalIdentifier, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: v4(), + isVisible, + size: DEFAULT_VIEW_FIELD_SIZE, + position: index, + aggregateOperation: null, + universalOverrides: null, + applicationUniversalIdentifier, + }); + }); + + if (customFields.length > 0) { + const otherGroupUniversalIdentifier = v4(); + + flatViewFieldGroupsToCreate.push({ + universalIdentifier: otherGroupUniversalIdentifier, + applicationUniversalIdentifier, + viewUniversalIdentifier, + name: 'Other', + position: 1, + isVisible: true, + overrides: null, + viewFieldUniversalIdentifiers: [], + createdAt, + updatedAt: createdAt, + deletedAt: null, + }); + + customFields.forEach((field, index) => { + const isVisible = + field.type !== FieldMetadataType.RELATION && + field.type !== FieldMetadataType.MORPH_RELATION; + + flatViewFieldsToCreate.push({ + fieldMetadataUniversalIdentifier: field.universalIdentifier, + viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier: otherGroupUniversalIdentifier, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: v4(), + isVisible, + size: DEFAULT_VIEW_FIELD_SIZE, + position: index, + aggregateOperation: null, + universalOverrides: null, + applicationUniversalIdentifier, + }); + }); + } + + return { + flatViewFieldGroupsToCreate, + flatViewFieldsToCreate, + }; +}; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 84a8db9b3b..8b29182def 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -8,8 +8,8 @@ export { IconAlignLeft, IconAlignRight, IconApi, - IconApps, IconAppWindow, + IconApps, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -17,12 +17,12 @@ export { IconArrowLeft, IconArrowMerge, IconArrowRight, + IconArrowUp, + IconArrowUpRight, IconArrowsDiagonal, IconArrowsSort, IconArrowsSplit2, IconArrowsVertical, - IconArrowUp, - IconArrowUpRight, IconAt, IconAxisX, IconAxisY, @@ -75,8 +75,8 @@ export { IconChevronLeft, IconChevronRight, IconChevronRightPipe, - IconChevronsRight, IconChevronUp, + IconChevronsRight, IconCircle, IconCircleDashed, IconCircleDot, @@ -181,8 +181,8 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, - IconFilters, IconFilterX, + IconFilters, IconFlag, IconFlask, IconFocusCentered, @@ -251,6 +251,7 @@ export { IconListCheck, IconListDetails, IconListNumbers, + IconListSearch, IconLoader, IconLock, IconLockOpen, @@ -338,10 +339,10 @@ export { IconShield, IconSitemap, IconSlash, + IconSortAZ, IconSortAscending, IconSortAscendingLetters, IconSortAscendingNumbers, - IconSortAZ, IconSortDescending, IconSortDescendingLetters, IconSortDescendingNumbers, @@ -360,6 +361,7 @@ export { IconSquareNumber7, IconSquareNumber8, IconSquareNumber9, + IconSquarePlus, IconSquareRoundedCheck, IconSquareRoundedX, IconStack2, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 740defef9f..fa9335a0a4 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -86,8 +86,8 @@ export { IconAlignLeft, IconAlignRight, IconApi, - IconApps, IconAppWindow, + IconApps, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -95,12 +95,12 @@ export { IconArrowLeft, IconArrowMerge, IconArrowRight, + IconArrowUp, + IconArrowUpRight, IconArrowsDiagonal, IconArrowsSort, IconArrowsSplit2, IconArrowsVertical, - IconArrowUp, - IconArrowUpRight, IconAt, IconAxisX, IconAxisY, @@ -153,8 +153,8 @@ export { IconChevronLeft, IconChevronRight, IconChevronRightPipe, - IconChevronsRight, IconChevronUp, + IconChevronsRight, IconCircle, IconCircleDashed, IconCircleDot, @@ -259,8 +259,8 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, - IconFilters, IconFilterX, + IconFilters, IconFlag, IconFlask, IconFocusCentered, @@ -329,6 +329,7 @@ export { IconListCheck, IconListDetails, IconListNumbers, + IconListSearch, IconLoader, IconLock, IconLockOpen, @@ -416,10 +417,10 @@ export { IconShield, IconSitemap, IconSlash, + IconSortAZ, IconSortAscending, IconSortAscendingLetters, IconSortAscendingNumbers, - IconSortAZ, IconSortDescending, IconSortDescendingLetters, IconSortDescendingNumbers, @@ -438,6 +439,7 @@ export { IconSquareNumber7, IconSquareNumber8, IconSquareNumber9, + IconSquarePlus, IconSquareRoundedCheck, IconSquareRoundedX, IconStack2,