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 <charles@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-03-23 00:44:49 +01:00
committed by GitHub
parent dc00701448
commit e9aa6f47e5
19 changed files with 1921 additions and 771 deletions
@@ -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 (
<PageLayoutVerticalListEditor
widgets={activeTab.widgets}
onReorder={reorderWidgets}
isReorderEnabled={true}
trailingElement={
isRecordPageLayout ? <RecordPageAddWidgetSection /> : undefined
}
/>
);
}
@@ -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 = ({
</Draggable>
))}
{provided.placeholder}
{trailingElement}
</StyledVerticalListContainer>
)}
</Droppable>
@@ -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 };
};
@@ -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 };
};
@@ -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 };
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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 (
<StyledContainer>
<StyledHeader>
<IconSquarePlus
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={theme.font.color.extraLight}
/>
{t`Add widget`}
</StyledHeader>
<StyledMenuItemList>
<MenuItem
LeftIcon={IconListDetails}
withIconContainer
text={t`Fields group`}
contextualText={t`Group multiple fields from this record`}
onClick={createRecordPageFieldsWidget}
/>
<MenuItem
LeftIcon={IconListSearch}
withIconContainer
text={t`Field`}
contextualText={t`Single field with smart formats`}
onClick={createRecordPageFieldWidget}
/>
<MenuItem
LeftIcon={IconPlus}
withIconContainer
text={t`More widgets`}
hasSubMenu
onClick={navigateToMoreWidgets}
/>
</StyledMenuItemList>
</StyledContainer>
);
};
@@ -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 = (
<WidgetCard
@@ -3095,6 +3095,12 @@ type Query {
getPageLayoutTab(id: String!): PageLayoutTab!
getPageLayouts(objectMetadataId: String, pageLayoutType: PageLayoutType): [PageLayout!]!
getPageLayout(id: String!): PageLayout
getViews(objectMetadataId: String, viewTypes: [ViewType!]): [View!]!
getView(id: String!): View
getViewSorts(viewId: String): [ViewSort!]!
getViewSort(id: String!): ViewSort
getViewFieldGroups(viewId: String!): [ViewFieldGroup!]!
getViewFieldGroup(id: String!): ViewFieldGroup
findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
findManyLogicFunctions: [LogicFunction!]!
getAvailablePackages(input: LogicFunctionIdInput!): JSON!
@@ -3117,12 +3123,6 @@ type Query {
): ObjectConnection!
getViewFields(viewId: String!): [ViewField!]!
getViewField(id: String!): ViewField
getViews(objectMetadataId: String, viewTypes: [ViewType!]): [View!]!
getView(id: String!): View
getViewSorts(viewId: String): [ViewSort!]!
getViewSort(id: String!): ViewSort
getViewFieldGroups(viewId: String!): [ViewFieldGroup!]!
getViewFieldGroup(id: String!): ViewFieldGroup
index(
"""The id of the record to find."""
id: UUID!
@@ -3362,6 +3362,20 @@ type Mutation {
updatePageLayout(id: String!, input: UpdatePageLayoutInput!): PageLayout!
destroyPageLayout(id: String!): Boolean!
updatePageLayoutWithTabsAndWidgets(id: String!, input: UpdatePageLayoutWithTabsInput!): PageLayout!
createView(input: CreateViewInput!): View!
updateView(id: String!, input: UpdateViewInput!): View!
deleteView(id: String!): Boolean!
destroyView(id: String!): Boolean!
createViewSort(input: CreateViewSortInput!): ViewSort!
updateViewSort(input: UpdateViewSortInput!): ViewSort!
deleteViewSort(input: DeleteViewSortInput!): Boolean!
destroyViewSort(input: DestroyViewSortInput!): Boolean!
updateViewFieldGroup(input: UpdateViewFieldGroupInput!): ViewFieldGroup!
createViewFieldGroup(input: CreateViewFieldGroupInput!): ViewFieldGroup!
createManyViewFieldGroups(inputs: [CreateViewFieldGroupInput!]!): [ViewFieldGroup!]!
deleteViewFieldGroup(input: DeleteViewFieldGroupInput!): ViewFieldGroup!
destroyViewFieldGroup(input: DestroyViewFieldGroupInput!): ViewFieldGroup!
upsertFieldsWidget(input: UpsertFieldsWidgetInput!): View!
deleteOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
createOneLogicFunction(input: CreateLogicFunctionFromSourceInput!): LogicFunction!
executeOneLogicFunction(input: ExecuteOneLogicFunctionInput!): LogicFunctionExecutionResult!
@@ -3380,20 +3394,6 @@ type Mutation {
createManyViewFields(inputs: [CreateViewFieldInput!]!): [ViewField!]!
deleteViewField(input: DeleteViewFieldInput!): ViewField!
destroyViewField(input: DestroyViewFieldInput!): ViewField!
createView(input: CreateViewInput!): View!
updateView(id: String!, input: UpdateViewInput!): View!
deleteView(id: String!): Boolean!
destroyView(id: String!): Boolean!
createViewSort(input: CreateViewSortInput!): ViewSort!
updateViewSort(input: UpdateViewSortInput!): ViewSort!
deleteViewSort(input: DeleteViewSortInput!): Boolean!
destroyViewSort(input: DestroyViewSortInput!): Boolean!
updateViewFieldGroup(input: UpdateViewFieldGroupInput!): ViewFieldGroup!
createViewFieldGroup(input: CreateViewFieldGroupInput!): ViewFieldGroup!
createManyViewFieldGroups(inputs: [CreateViewFieldGroupInput!]!): [ViewFieldGroup!]!
deleteViewFieldGroup(input: DeleteViewFieldGroupInput!): ViewFieldGroup!
destroyViewFieldGroup(input: DestroyViewFieldGroupInput!): ViewFieldGroup!
upsertFieldsWidget(input: UpsertFieldsWidgetInput!): View!
createOneAgent(input: CreateAgentInput!): Agent!
updateOneAgent(input: UpdateAgentInput!): Agent!
deleteOneAgent(input: AgentIdInput!): Agent!
@@ -3675,6 +3675,136 @@ input UpdatePageLayoutWidgetWithIdInput {
conditionalDisplay: JSON
}
input CreateViewInput {
id: UUID
name: String!
objectMetadataId: UUID!
type: ViewType = TABLE
key: ViewKey
icon: String!
position: Float = 0
isCompact: Boolean = false
shouldHideEmptyGroups: Boolean = false
openRecordIn: ViewOpenRecordIn = SIDE_PANEL
kanbanAggregateOperation: AggregateOperations
kanbanAggregateOperationFieldMetadataId: UUID
anyFieldFilterValue: String
calendarLayout: ViewCalendarLayout
calendarFieldMetadataId: UUID
mainGroupByFieldMetadataId: UUID
visibility: ViewVisibility
}
input UpdateViewInput {
id: UUID
name: String
type: ViewType
icon: String
position: Float
isCompact: Boolean
openRecordIn: ViewOpenRecordIn
kanbanAggregateOperation: AggregateOperations
kanbanAggregateOperationFieldMetadataId: UUID
anyFieldFilterValue: String
calendarLayout: ViewCalendarLayout
calendarFieldMetadataId: UUID
visibility: ViewVisibility
mainGroupByFieldMetadataId: UUID
shouldHideEmptyGroups: Boolean
}
input CreateViewSortInput {
id: UUID
fieldMetadataId: UUID!
direction: ViewSortDirection = ASC
viewId: UUID!
}
input UpdateViewSortInput {
"""The id of the view sort to update"""
id: UUID!
"""The view sort to update"""
update: UpdateViewSortInputUpdates!
}
input UpdateViewSortInputUpdates {
direction: ViewSortDirection
}
input DeleteViewSortInput {
"""The id of the view sort to delete."""
id: UUID!
}
input DestroyViewSortInput {
"""The id of the view sort to destroy."""
id: UUID!
}
input UpdateViewFieldGroupInput {
"""The id of the view field group to update"""
id: UUID!
"""The view field group to update"""
update: UpdateViewFieldGroupInputUpdates!
}
input UpdateViewFieldGroupInputUpdates {
name: String
position: Float
isVisible: Boolean
deletedAt: String
}
input CreateViewFieldGroupInput {
id: UUID
name: String!
viewId: UUID!
position: Float = 0
isVisible: Boolean = true
}
input DeleteViewFieldGroupInput {
"""The id of the view field group to delete."""
id: UUID!
}
input DestroyViewFieldGroupInput {
"""The id of the view field group to destroy."""
id: UUID!
}
input UpsertFieldsWidgetInput {
"""The id of the fields widget whose groups and fields to upsert"""
widgetId: UUID!
"""
The groups (with nested fields) to upsert. Mutually exclusive with "fields".
"""
groups: [UpsertFieldsWidgetGroupInput!]
"""
The ungrouped fields to upsert. When provided, all existing groups are deleted and fields are detached from groups. Mutually exclusive with "groups".
"""
fields: [UpsertFieldsWidgetFieldInput!]
}
input UpsertFieldsWidgetGroupInput {
id: UUID!
name: String!
position: Float!
isVisible: Boolean!
fields: [UpsertFieldsWidgetFieldInput!]!
}
input UpsertFieldsWidgetFieldInput {
"""The id of the view field"""
viewFieldId: UUID!
isVisible: Boolean!
position: Float!
}
input CreateLogicFunctionFromSourceInput {
id: UUID
universalIdentifier: UUID
@@ -3855,136 +3985,6 @@ input DestroyViewFieldInput {
id: UUID!
}
input CreateViewInput {
id: UUID
name: String!
objectMetadataId: UUID!
type: ViewType = TABLE
key: ViewKey
icon: String!
position: Float = 0
isCompact: Boolean = false
shouldHideEmptyGroups: Boolean = false
openRecordIn: ViewOpenRecordIn = SIDE_PANEL
kanbanAggregateOperation: AggregateOperations
kanbanAggregateOperationFieldMetadataId: UUID
anyFieldFilterValue: String
calendarLayout: ViewCalendarLayout
calendarFieldMetadataId: UUID
mainGroupByFieldMetadataId: UUID
visibility: ViewVisibility
}
input UpdateViewInput {
id: UUID
name: String
type: ViewType
icon: String
position: Float
isCompact: Boolean
openRecordIn: ViewOpenRecordIn
kanbanAggregateOperation: AggregateOperations
kanbanAggregateOperationFieldMetadataId: UUID
anyFieldFilterValue: String
calendarLayout: ViewCalendarLayout
calendarFieldMetadataId: UUID
visibility: ViewVisibility
mainGroupByFieldMetadataId: UUID
shouldHideEmptyGroups: Boolean
}
input CreateViewSortInput {
id: UUID
fieldMetadataId: UUID!
direction: ViewSortDirection = ASC
viewId: UUID!
}
input UpdateViewSortInput {
"""The id of the view sort to update"""
id: UUID!
"""The view sort to update"""
update: UpdateViewSortInputUpdates!
}
input UpdateViewSortInputUpdates {
direction: ViewSortDirection
}
input DeleteViewSortInput {
"""The id of the view sort to delete."""
id: UUID!
}
input DestroyViewSortInput {
"""The id of the view sort to destroy."""
id: UUID!
}
input UpdateViewFieldGroupInput {
"""The id of the view field group to update"""
id: UUID!
"""The view field group to update"""
update: UpdateViewFieldGroupInputUpdates!
}
input UpdateViewFieldGroupInputUpdates {
name: String
position: Float
isVisible: Boolean
deletedAt: String
}
input CreateViewFieldGroupInput {
id: UUID
name: String!
viewId: UUID!
position: Float = 0
isVisible: Boolean = true
}
input DeleteViewFieldGroupInput {
"""The id of the view field group to delete."""
id: UUID!
}
input DestroyViewFieldGroupInput {
"""The id of the view field group to destroy."""
id: UUID!
}
input UpsertFieldsWidgetInput {
"""The id of the fields widget whose groups and fields to upsert"""
widgetId: UUID!
"""
The groups (with nested fields) to upsert. Mutually exclusive with "fields".
"""
groups: [UpsertFieldsWidgetGroupInput!]
"""
The ungrouped fields to upsert. When provided, all existing groups are deleted and fields are detached from groups. Mutually exclusive with "groups".
"""
fields: [UpsertFieldsWidgetFieldInput!]
}
input UpsertFieldsWidgetGroupInput {
id: UUID!
name: String!
position: Float!
isVisible: Boolean!
fields: [UpsertFieldsWidgetFieldInput!]!
}
input UpsertFieldsWidgetFieldInput {
"""The id of the view field"""
viewFieldId: UUID!
isVisible: Boolean!
position: Float!
}
input CreateAgentInput {
name: String
label: String!
@@ -2688,6 +2688,12 @@ export interface Query {
getPageLayoutTab: PageLayoutTab
getPageLayouts: PageLayout[]
getPageLayout?: PageLayout
getViews: View[]
getView?: View
getViewSorts: ViewSort[]
getViewSort?: ViewSort
getViewFieldGroups: ViewFieldGroup[]
getViewFieldGroup?: ViewFieldGroup
findOneLogicFunction: LogicFunction
findManyLogicFunctions: LogicFunction[]
getAvailablePackages: Scalars['JSON']
@@ -2701,12 +2707,6 @@ export interface Query {
objects: ObjectConnection
getViewFields: ViewField[]
getViewField?: ViewField
getViews: View[]
getView?: View
getViewSorts: ViewSort[]
getViewSort?: ViewSort
getViewFieldGroups: ViewFieldGroup[]
getViewFieldGroup?: ViewFieldGroup
index: Index
indexMetadatas: IndexConnection
findManyAgents: Agent[]
@@ -2827,6 +2827,20 @@ export interface Mutation {
updatePageLayout: PageLayout
destroyPageLayout: Scalars['Boolean']
updatePageLayoutWithTabsAndWidgets: PageLayout
createView: View
updateView: View
deleteView: Scalars['Boolean']
destroyView: Scalars['Boolean']
createViewSort: ViewSort
updateViewSort: ViewSort
deleteViewSort: Scalars['Boolean']
destroyViewSort: Scalars['Boolean']
updateViewFieldGroup: ViewFieldGroup
createViewFieldGroup: ViewFieldGroup
createManyViewFieldGroups: ViewFieldGroup[]
deleteViewFieldGroup: ViewFieldGroup
destroyViewFieldGroup: ViewFieldGroup
upsertFieldsWidget: View
deleteOneLogicFunction: LogicFunction
createOneLogicFunction: LogicFunction
executeOneLogicFunction: LogicFunctionExecutionResult
@@ -2845,20 +2859,6 @@ export interface Mutation {
createManyViewFields: ViewField[]
deleteViewField: ViewField
destroyViewField: ViewField
createView: View
updateView: View
deleteView: Scalars['Boolean']
destroyView: Scalars['Boolean']
createViewSort: ViewSort
updateViewSort: ViewSort
deleteViewSort: Scalars['Boolean']
destroyViewSort: Scalars['Boolean']
updateViewFieldGroup: ViewFieldGroup
createViewFieldGroup: ViewFieldGroup
createManyViewFieldGroups: ViewFieldGroup[]
deleteViewFieldGroup: ViewFieldGroup
destroyViewFieldGroup: ViewFieldGroup
upsertFieldsWidget: View
createOneAgent: Agent
updateOneAgent: Agent
deleteOneAgent: Agent
@@ -5848,6 +5848,12 @@ export interface QueryGenqlSelection{
getPageLayoutTab?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
getPageLayouts?: (PageLayoutGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), pageLayoutType?: (PageLayoutType | null)} })
getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
getViews?: (ViewGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), viewTypes?: (ViewType[] | null)} })
getView?: (ViewGenqlSelection & { __args: {id: Scalars['String']} })
getViewSorts?: (ViewSortGenqlSelection & { __args?: {viewId?: (Scalars['String'] | null)} })
getViewSort?: (ViewSortGenqlSelection & { __args: {id: Scalars['String']} })
getViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {viewId: Scalars['String']} })
getViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {id: Scalars['String']} })
findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
findManyLogicFunctions?: LogicFunctionGenqlSelection
getAvailablePackages?: { __args: {input: LogicFunctionIdInput} }
@@ -5867,12 +5873,6 @@ export interface QueryGenqlSelection{
filter: ObjectFilter} })
getViewFields?: (ViewFieldGenqlSelection & { __args: {viewId: Scalars['String']} })
getViewField?: (ViewFieldGenqlSelection & { __args: {id: Scalars['String']} })
getViews?: (ViewGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), viewTypes?: (ViewType[] | null)} })
getView?: (ViewGenqlSelection & { __args: {id: Scalars['String']} })
getViewSorts?: (ViewSortGenqlSelection & { __args?: {viewId?: (Scalars['String'] | null)} })
getViewSort?: (ViewSortGenqlSelection & { __args: {id: Scalars['String']} })
getViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {viewId: Scalars['String']} })
getViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {id: Scalars['String']} })
index?: (IndexGenqlSelection & { __args: {
/** The id of the record to find. */
id: Scalars['UUID']} })
@@ -6030,6 +6030,20 @@ export interface MutationGenqlSelection{
updatePageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutInput} })
destroyPageLayout?: { __args: {id: Scalars['String']} }
updatePageLayoutWithTabsAndWidgets?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWithTabsInput} })
createView?: (ViewGenqlSelection & { __args: {input: CreateViewInput} })
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
deleteView?: { __args: {id: Scalars['String']} }
destroyView?: { __args: {id: Scalars['String']} }
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
destroyViewSort?: { __args: {input: DestroyViewSortInput} }
updateViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: UpdateViewFieldGroupInput} })
createViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: CreateViewFieldGroupInput} })
createManyViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {inputs: CreateViewFieldGroupInput[]} })
deleteViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DeleteViewFieldGroupInput} })
destroyViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DestroyViewFieldGroupInput} })
upsertFieldsWidget?: (ViewGenqlSelection & { __args: {input: UpsertFieldsWidgetInput} })
deleteOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
createOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: CreateLogicFunctionFromSourceInput} })
executeOneLogicFunction?: (LogicFunctionExecutionResultGenqlSelection & { __args: {input: ExecuteOneLogicFunctionInput} })
@@ -6048,20 +6062,6 @@ export interface MutationGenqlSelection{
createManyViewFields?: (ViewFieldGenqlSelection & { __args: {inputs: CreateViewFieldInput[]} })
deleteViewField?: (ViewFieldGenqlSelection & { __args: {input: DeleteViewFieldInput} })
destroyViewField?: (ViewFieldGenqlSelection & { __args: {input: DestroyViewFieldInput} })
createView?: (ViewGenqlSelection & { __args: {input: CreateViewInput} })
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
deleteView?: { __args: {id: Scalars['String']} }
destroyView?: { __args: {id: Scalars['String']} }
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
destroyViewSort?: { __args: {input: DestroyViewSortInput} }
updateViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: UpdateViewFieldGroupInput} })
createViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: CreateViewFieldGroupInput} })
createManyViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {inputs: CreateViewFieldGroupInput[]} })
deleteViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DeleteViewFieldGroupInput} })
destroyViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DestroyViewFieldGroupInput} })
upsertFieldsWidget?: (ViewGenqlSelection & { __args: {input: UpsertFieldsWidgetInput} })
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
@@ -6247,6 +6247,60 @@ export interface UpdatePageLayoutTabWithWidgetsInput {id: Scalars['UUID'],title:
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
export interface UpdateViewSortInput {
/** The id of the view sort to update */
id: Scalars['UUID'],
/** The view sort to update */
update: UpdateViewSortInputUpdates}
export interface UpdateViewSortInputUpdates {direction?: (ViewSortDirection | null)}
export interface DeleteViewSortInput {
/** The id of the view sort to delete. */
id: Scalars['UUID']}
export interface DestroyViewSortInput {
/** The id of the view sort to destroy. */
id: Scalars['UUID']}
export interface UpdateViewFieldGroupInput {
/** The id of the view field group to update */
id: Scalars['UUID'],
/** The view field group to update */
update: UpdateViewFieldGroupInputUpdates}
export interface UpdateViewFieldGroupInputUpdates {name?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isVisible?: (Scalars['Boolean'] | null),deletedAt?: (Scalars['String'] | null)}
export interface CreateViewFieldGroupInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],viewId: Scalars['UUID'],position?: (Scalars['Float'] | null),isVisible?: (Scalars['Boolean'] | null)}
export interface DeleteViewFieldGroupInput {
/** The id of the view field group to delete. */
id: Scalars['UUID']}
export interface DestroyViewFieldGroupInput {
/** The id of the view field group to destroy. */
id: Scalars['UUID']}
export interface UpsertFieldsWidgetInput {
/** The id of the fields widget whose groups and fields to upsert */
widgetId: Scalars['UUID'],
/** The groups (with nested fields) to upsert. Mutually exclusive with "fields". */
groups?: (UpsertFieldsWidgetGroupInput[] | null),
/** The ungrouped fields to upsert. When provided, all existing groups are deleted and fields are detached from groups. Mutually exclusive with "groups". */
fields?: (UpsertFieldsWidgetFieldInput[] | null)}
export interface UpsertFieldsWidgetGroupInput {id: Scalars['UUID'],name: Scalars['String'],position: Scalars['Float'],isVisible: Scalars['Boolean'],fields: UpsertFieldsWidgetFieldInput[]}
export interface UpsertFieldsWidgetFieldInput {
/** The id of the view field */
viewFieldId: Scalars['UUID'],isVisible: Scalars['Boolean'],position: Scalars['Float']}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
export interface ExecuteOneLogicFunctionInput {
@@ -6311,60 +6365,6 @@ export interface DestroyViewFieldInput {
/** The id of the view field to destroy. */
id: Scalars['UUID']}
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
export interface UpdateViewSortInput {
/** The id of the view sort to update */
id: Scalars['UUID'],
/** The view sort to update */
update: UpdateViewSortInputUpdates}
export interface UpdateViewSortInputUpdates {direction?: (ViewSortDirection | null)}
export interface DeleteViewSortInput {
/** The id of the view sort to delete. */
id: Scalars['UUID']}
export interface DestroyViewSortInput {
/** The id of the view sort to destroy. */
id: Scalars['UUID']}
export interface UpdateViewFieldGroupInput {
/** The id of the view field group to update */
id: Scalars['UUID'],
/** The view field group to update */
update: UpdateViewFieldGroupInputUpdates}
export interface UpdateViewFieldGroupInputUpdates {name?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isVisible?: (Scalars['Boolean'] | null),deletedAt?: (Scalars['String'] | null)}
export interface CreateViewFieldGroupInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],viewId: Scalars['UUID'],position?: (Scalars['Float'] | null),isVisible?: (Scalars['Boolean'] | null)}
export interface DeleteViewFieldGroupInput {
/** The id of the view field group to delete. */
id: Scalars['UUID']}
export interface DestroyViewFieldGroupInput {
/** The id of the view field group to destroy. */
id: Scalars['UUID']}
export interface UpsertFieldsWidgetInput {
/** The id of the fields widget whose groups and fields to upsert */
widgetId: Scalars['UUID'],
/** The groups (with nested fields) to upsert. Mutually exclusive with "fields". */
groups?: (UpsertFieldsWidgetGroupInput[] | null),
/** The ungrouped fields to upsert. When provided, all existing groups are deleted and fields are detached from groups. Mutually exclusive with "groups". */
fields?: (UpsertFieldsWidgetFieldInput[] | null)}
export interface UpsertFieldsWidgetGroupInput {id: Scalars['UUID'],name: Scalars['String'],position: Scalars['Float'],isVisible: Scalars['Boolean'],fields: UpsertFieldsWidgetFieldInput[]}
export interface UpsertFieldsWidgetFieldInput {
/** The id of the view field */
viewFieldId: Scalars['UUID'],isVisible: Scalars['Boolean'],position: Scalars['Float']}
export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@ import { PageLayoutDuplicationService } from 'src/engine/metadata-modules/page-l
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -37,6 +38,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
FlatPageLayoutWidgetModule,
ApplicationModule,
DashboardSyncModule,
ViewModule,
],
controllers: [PageLayoutController],
providers: [
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
@@ -21,6 +21,7 @@ import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layou
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab-with-widgets.input';
import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
import {
@@ -30,6 +31,7 @@ import {
generatePageLayoutExceptionMessage,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
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 { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
@@ -42,11 +44,14 @@ type UpdatePageLayoutWithTabsParams = {
@Injectable()
export class PageLayoutUpdateService {
private readonly logger = new Logger(PageLayoutUpdateService.name);
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
private readonly dashboardSyncService: DashboardSyncService,
private readonly viewService: ViewService,
) {}
async updatePageLayoutWithTabs({
@@ -161,6 +166,12 @@ export class PageLayoutUpdateService {
flatViewMaps,
});
const orphanedViewIds = this.collectOrphanedViewIdsFromDeletedWidgets({
widgetsToUpdate,
tabsToUpdate,
flatPageLayoutWidgetMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -223,6 +234,11 @@ export class PageLayoutUpdateService {
},
);
await this.destroyOrphanedFieldsWidgetViews({
viewIds: orphanedViewIds,
workspaceId,
});
return fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
reconstructFlatPageLayoutWithTabsAndWidgets({
layout: flatLayout,
@@ -617,4 +633,108 @@ export class PageLayoutUpdateService {
],
};
}
private collectOrphanedViewIdsFromDeletedWidgets({
widgetsToUpdate,
tabsToUpdate,
flatPageLayoutWidgetMaps,
}: {
widgetsToUpdate: FlatPageLayoutWidget[];
tabsToUpdate: FlatPageLayoutTab[];
flatPageLayoutWidgetMaps: Pick<
AllFlatEntityMaps,
'flatPageLayoutWidgetMaps'
>['flatPageLayoutWidgetMaps'];
}): string[] {
const viewIdsToDelete = new Set<string>();
const directlyDeletedWidgetIds = new Set<string>();
// 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<void> {
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}`,
);
}
}
}
}
@@ -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<typeof field> =>
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,
@@ -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<UniversalFlatFieldMetadata> & {
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);
});
});
@@ -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,
};
};
@@ -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,
+8 -6
View File
@@ -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,