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,