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:
committed by
GitHub
parent
dc00701448
commit
e9aa6f47e5
@@ -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
-3
@@ -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 };
|
||||
};
|
||||
+101
@@ -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,
|
||||
};
|
||||
};
|
||||
+90
@@ -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>
|
||||
);
|
||||
};
|
||||
+18
-4
@@ -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: [
|
||||
|
||||
+121
-1
@@ -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,
|
||||
|
||||
+415
@@ -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);
|
||||
});
|
||||
});
|
||||
+137
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user