Compare commits

..
Author SHA1 Message Date
Thomas Trompette c5ebe7a757 WIP 2026-03-26 17:39:37 +01:00
50 changed files with 5721 additions and 5004 deletions
@@ -6,13 +6,11 @@ import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelate
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
FindAllViewsDocument,
FindManyCommandMenuItemsDocument,
FindAllRecordPageLayoutsDocument,
@@ -57,9 +55,6 @@ export const useLoadStaleMetadataEntities = () => {
const client = useApolloClient();
const store = useStore();
const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft();
const isCommandMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
);
const loadStaleMetadataEntities = useCallback(
async (staleEntityKeys: MetadataEntityKey[]) => {
@@ -199,10 +194,7 @@ export const useLoadStaleMetadataEntities = () => {
);
}
if (
staleEntityKeys.includes('commandMenuItems') &&
isCommandMenuItemEnabled
) {
if (staleEntityKeys.includes('commandMenuItems')) {
fetchPromises.push(
client
.query({
@@ -222,7 +214,7 @@ export const useLoadStaleMetadataEntities = () => {
await Promise.all(fetchPromises);
applyChanges();
},
[client, store, replaceDraft, applyChanges, isCommandMenuItemEnabled],
[client, store, replaceDraft, applyChanges],
);
return { loadStaleMetadataEntities };
@@ -8,17 +8,14 @@ const StyledContainer = styled.div`
align-items: center;
background: ${themeCssVariables.background.transparent.light};
border-radius: ${themeCssVariables.border.radius.sm};
border-radius: 4px;
color: ${themeCssVariables.font.color.tertiary};
display: inline-flex;
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[3]};
overflow: hidden;
padding: ${themeCssVariables.spacing[1]};
user-select: none;
`;
export const ForbiddenFieldDisplay = () => {
@@ -25,6 +25,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
loading,
totalCount,
fetchMoreRecords,
error,
} = useFindManyRecords({
...params,
recordGqlFields,
@@ -38,5 +39,6 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
queryIdentifier,
totalCount,
fetchMoreRecords,
error,
};
};
@@ -1,11 +1,12 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateAccessDenied } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateAccessDenied';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateReadOnly } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateReadOnly';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -13,10 +14,12 @@ export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
const isRecordTableAccessDenied = useAtomComponentStateValue(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const isRemote = objectMetadataItem.isRemote;
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const isSoftDeleteFilterActive = useAtomComponentStateValue(
isSoftDeleteFilterActiveComponentState,
@@ -27,15 +30,18 @@ export const RecordTableEmptyState = () => {
objectMetadataItem.id,
);
if (isRecordTableAccessDenied) {
return <RecordTableEmptyStateAccessDenied />;
}
const noRecordAtAll = totalCount === 0;
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return <RecordTableEmptyStateReadOnly />;
}
if (isRemote) {
return <RecordTableEmptyStateRemote />;
} else if (isSoftDeleteFilterActive === true) {
if (isSoftDeleteFilterActive === true) {
return <RecordTableEmptyStateSoftDelete />;
} else if (noRecordAtAll) {
return <RecordTableEmptyStateNoGroupNoRecordAtAll />;
@@ -1,25 +0,0 @@
/* oxlint-disable twenty/no-navigate-prefer-link */
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { IconSettings } from 'twenty-ui/display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const RecordTableEmptyStateRemote = () => {
const navigate = useNavigateSettings();
const handleButtonClick = () => {
navigate(SettingsPath.Integrations);
};
return (
<RecordTableEmptyStateDisplay
buttonTitle={t`Go to Settings`}
subTitle={t`If this is unexpected, please verify your settings.`}
title={t`No Data Available for Remote Table`}
ButtonIcon={IconSettings}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>
);
};
@@ -1,44 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ScrollWrapperDecorator } from '~/testing/decorators/ScrollWrapperDecorator';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
component: RecordTableEmptyStateRemote,
decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator,
MemoryRouterDecorator,
RecordTableDecorator,
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
ScrollWrapperDecorator,
],
parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateRemote>;
export const Default: Story = {};
@@ -1,4 +1,7 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isNonEmptyString } from '@sniptt/guards';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -10,10 +13,10 @@ import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { isNonEmptyString } from '@sniptt/guards';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();
@@ -27,9 +30,14 @@ export const RecordTableRecordGroupBodyEffect = () => {
isRecordTableInitialLoadingComponentState,
);
const setIsAccessDenied = useSetAtomComponentState(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const recordGroupId = useCurrentRecordGroupId();
const { records, loading, hasNextPage } =
const { records, loading, hasNextPage, error } =
useRecordIndexTableQuery(objectNameSingular);
const setRecordIndexHasFetchedAllRecordsByGroup =
@@ -43,7 +51,21 @@ export const RecordTableRecordGroupBodyEffect = () => {
const { scrollToPosition } = useScrollToPosition();
useEffect(() => {
if (isDefined(error) && CombinedGraphQLErrors.is(error)) {
const isForbidden = error.errors.some(
(graphqlError) => graphqlError.extensions?.code === 'FORBIDDEN',
);
if (isForbidden) {
setIsAccessDenied(true);
setIsRecordTableInitialLoading(false);
return;
}
}
if (!loading) {
setIsAccessDenied(false);
setRecordTableData({
records,
currentRecordGroupId: recordGroupId,
@@ -52,10 +74,12 @@ export const RecordTableRecordGroupBodyEffect = () => {
setRecordIndexHasFetchedAllRecordsByGroup(!hasNextPage);
}
}, [
error,
hasNextPage,
loading,
records,
recordGroupId,
setIsAccessDenied,
setRecordIndexHasFetchedAllRecordsByGroup,
setIsRecordTableInitialLoading,
setRecordTableData,
@@ -1,5 +1,8 @@
import { type ErrorLike } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { useRecordIndexTableLazyQuery } from '@/object-record/record-index/hooks/useRecordIndexTableLazyQuery';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
@@ -11,10 +14,10 @@ import { useScrollTableToPosition } from '@/object-record/record-table/hooks/use
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { updateRecordTableCSSVariable } from '@/object-record/record-table/utils/updateRecordTableCSSVariable';
import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows';
import { useReapplyRowSelection } from '@/object-record/record-table/virtualization/hooks/useReapplyRowSelection';
import { useResetTableFocuses } from '@/object-record/record-table/virtualization/hooks/useResetTableFocuses';
import { useResetVirtualizedRowTreadmill } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizedRowTreadmill';
import { dataPagesLoadedComponentState } from '@/object-record/record-table/virtualization/states/dataPagesLoadedComponentState';
@@ -31,7 +34,16 @@ import { dataLoadingStatusByRealIndexComponentState } from '@/object-record/reco
import { recordIdByRealIndexComponentState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { isDefined } from 'twenty-shared/utils';
const isForbiddenError = (error: ErrorLike): boolean => {
if (!CombinedGraphQLErrors.is(error)) {
return false;
}
return error.errors.some(
(graphqlError) => graphqlError.extensions?.code === 'FORBIDDEN',
);
};
export const useTriggerInitialRecordTableDataLoad = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
@@ -107,6 +119,11 @@ export const useTriggerInitialRecordTableDataLoad = () => {
totalNumberOfRecordsToVirtualizeComponentState,
);
const isRecordTableAccessDenied = useAtomComponentStateCallbackState(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const triggerInitialRecordTableDataLoad = useCallback(
async ({
shouldScrollToStart = true,
@@ -120,6 +137,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
}
store.set(isInitializingVirtualTableDataLoadingCallbackState, true);
store.set(isRecordTableAccessDenied, false);
try {
resetTableFocuses();
@@ -165,8 +183,23 @@ export const useTriggerInitialRecordTableDataLoad = () => {
newDataLoadingStatusByRealIndex,
);
const { records: findManyRecords, totalCount: findManyTotalCount } =
await findManyRecordsLazy();
const {
records: findManyRecords,
totalCount: findManyTotalCount,
error: queryError,
} = await findManyRecordsLazy();
if (isDefined(queryError) && isForbiddenError(queryError)) {
store.set(isRecordTableAccessDenied, true);
store.set(totalNumberOfRecordsToVirtualizeCallbackState, 0);
store.set(recordIdByRealIndexCallbackState, new Map());
store.set(
dataLoadingStatusByRealIndexCallbackState,
new Map(),
);
return;
}
records = findManyRecords;
totalCount = findManyTotalCount;
@@ -201,6 +234,18 @@ export const useTriggerInitialRecordTableDataLoad = () => {
verticalScrollInPx: 0,
});
}
} catch (error) {
if (
isDefined(error) &&
typeof error === 'object' &&
'name' in error &&
isForbiddenError(error as ErrorLike)
) {
store.set(isRecordTableAccessDenied, true);
}
store.set(totalNumberOfRecordsToVirtualizeCallbackState, 0);
store.set(recordIdByRealIndexCallbackState, new Map());
store.set(dataLoadingStatusByRealIndexCallbackState, new Map());
} finally {
store.set(isInitializingVirtualTableDataLoadingCallbackState, false);
store.set(isRecordTableInitialLoading, false);
@@ -229,6 +274,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
loadRecordsToVirtualRows,
reapplyRowSelection,
recordTableId,
isRecordTableAccessDenied,
],
);
@@ -76,12 +76,6 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
if (widgetType === WidgetType.STANDALONE_RICH_TEXT) {
setPageLayoutEditingWidgetId(widgetId);
closeSidePanelMenu();
return;
}
setSidePanelPage(SidePanelPages.CommandMenuDisplay);
closeSidePanelMenu();
},
@@ -1,20 +1,8 @@
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/side-panel/constants/SidePanelContextChipGroupsDropdownId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/sidePanelNavigationMorphItemsByPageState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
@@ -22,10 +10,19 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { emitSidePanelCloseEvent } from '@/ui/layout/side-panel/utils/emitSidePanelCloseEvent';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { emitSidePanelCloseEvent } from '@/ui/layout/side-panel/utils/emitSidePanelCloseEvent';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowLogicFunctionTabListComponentId';
import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId';
@@ -44,12 +41,6 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
const { closeDropdown } = useCloseDropdown();
const resetNavigationMenuItemState = () => {
store.set(selectedNavigationMenuItemIdInEditModeState.atom, null);
store.set(pendingInsertionNavigationMenuItemState.atom, null);
store.set(addToNavPayloadRegistryState.atom, new Map());
};
const sidePanelCloseAnimationCompleteCleanup = useCallback(
(options?: { emitSidePanelCloseEvent?: boolean }) => {
closeDropdown(SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
@@ -119,7 +110,6 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
store.set(sidePanelShowHiddenObjectsState.atom, false);
store.set(sidePanelNavigationMorphItemsByPageState.atom, new Map());
store.set(sidePanelNavigationStackState.atom, []);
resetNavigationMenuItemState();
resetSelectedItem();
store.set(hasUserSelectedSidePanelListItemState.atom, false);
@@ -146,13 +136,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
);
}
},
[
closeDropdown,
resetContextStoreStates,
resetNavigationMenuItemState,
resetSelectedItem,
store,
],
[closeDropdown, resetContextStoreStates, resetSelectedItem, store],
);
return {
@@ -1,4 +1,5 @@
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
@@ -28,6 +29,7 @@ export const useSidePanelMenu = () => {
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
if (isSidePanelOpened) {
store.set(addToNavPayloadRegistryState.atom, new Map());
store.set(isSidePanelOpenedState.atom, false);
store.set(isSidePanelClosingState.atom, true);
closeAnyOpenDropdown();
@@ -92,17 +92,26 @@ export const useComputeRecordRelationFilterLabelValue = ({
? `${labelValueItems.length} ${relationObjectLabelPlural.toLowerCase()}`
: labelValueItems.join(', ');
if (labelValueItems.length > 0) {
return {
labelValue: getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
displayValue: filterDisplayValue,
},
}),
};
}
const hasUnresolvedRecords =
selectedRecordIds.length > 0 && selectedRecords.length === 0;
return {
labelValue:
labelValueItems.length > 0
? getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
displayValue: filterDisplayValue,
},
})
: getRecordFilterChipLabelValue({
recordFilter,
}),
labelValue: getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
...(hasUnresolvedRecords ? { displayValue: t`Not found` } : {}),
},
}),
};
};
@@ -0,0 +1,431 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
CoreObjectNameSingular,
FieldMetadataType,
PageLayoutTabLayoutMode,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
GRID_POSITIONS,
VERTICAL_LIST_LAYOUT_POSITIONS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const HOME_TAB_POSITION = 10;
const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
const settings = field.settings as
| { junctionTargetFieldId?: string }
| undefined;
return (
isDefined(settings?.junctionTargetFieldId) &&
typeof settings?.junctionTargetFieldId === 'string' &&
settings.junctionTargetFieldId.length > 0
);
};
const isRelationTargetAvailable = (
targetObject: FlatObjectMetadata | undefined,
): boolean => {
if (!isDefined(targetObject)) {
return false;
}
if (targetObject.isRemote) {
return false;
}
if (
targetObject.isSystem &&
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
) {
return false;
}
return true;
};
@Command({
name: 'upgrade:1-20:backfill-field-widgets',
description:
'Backfill FIELD widgets for relation fields in existing page layouts',
})
export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of FIELD widgets for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
]);
// Build a set of fieldMetadataIds that already have a FIELD widget
const existingFieldWidgetFieldIds = new Set<string>();
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)) {
if (
isDefined(widget) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
isDefined(widget.configuration.fieldMetadataId)
) {
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
}
}
// Build object ID → objectMetadata map
const objectById = new Map<string, FlatObjectMetadata>();
for (const obj of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (isDefined(obj)) {
objectById.set(obj.id, obj);
}
}
// Build object ID → RECORD_PAGE layout map
const recordPageLayoutByObjectId = new Map<
string,
{ id: string; universalIdentifier: string }
>();
for (const layout of Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)) {
if (
isDefined(layout) &&
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId)
) {
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
id: layout.id,
universalIdentifier: layout.universalIdentifier,
});
}
}
// Build pageLayoutId → home tab map
const homeTabByPageLayoutId = new Map<
string,
{
id: string;
universalIdentifier: string;
widgetCount: number;
}
>();
for (const tab of Object.values(
flatPageLayoutTabMaps.byUniversalIdentifier,
)) {
if (
isDefined(tab) &&
tab.position === HOME_TAB_POSITION &&
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
) {
homeTabByPageLayoutId.set(tab.pageLayoutId, {
id: tab.id,
universalIdentifier: tab.universalIdentifier,
widgetCount: tab.widgetIds?.length ?? 0,
});
}
}
// Group fields by objectMetadataId
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
// Map morphId → all field IDs sharing that morphId (for dedup)
const fieldIdsByMorphId = new Map<string, string[]>();
for (const field of Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
continue;
}
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
list.push(field);
fieldsByObjectId.set(field.objectMetadataId, list);
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
morphFieldIds.push(field.id);
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
}
}
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
const pageLayout = recordPageLayoutByObjectId.get(object.id);
if (!isDefined(pageLayout)) {
continue;
}
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
if (!isDefined(homeTab)) {
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
for (const field of fields) {
if (
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION
) {
continue;
}
if (isActivityTargetField(field.name, object.nameSingular)) {
continue;
}
if (isJunctionRelationField(field)) {
continue;
}
if (field.type === FieldMetadataType.RELATION) {
const targetObject = isDefined(field.relationTargetObjectMetadataId)
? objectById.get(field.relationTargetObjectMetadataId)
: undefined;
if (!isRelationTargetAvailable(targetObject)) {
continue;
}
}
// For morph relations, skip if any sibling field (same morphId)
// already has a widget or was already processed in this run
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
if (processedMorphIds.has(field.morphId)) {
continue;
}
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
const alreadyHasWidget = siblingFieldIds.some((id) =>
existingFieldWidgetFieldIds.has(id),
);
if (alreadyHasWidget) {
continue;
}
processedMorphIds.add(field.morphId);
}
if (existingFieldWidgetFieldIds.has(field.id)) {
continue;
}
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
title: field.label,
type: WidgetType.FIELD,
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
position: {
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
index: nextWidgetIndex,
},
configuration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
universalConfiguration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.universalIdentifier,
fieldDisplayMode: FieldDisplayMode.CARD,
},
objectMetadataId: object.id,
objectMetadataUniversalIdentifier: object.universalIdentifier,
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
overrides: null,
};
targetList.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
return;
}
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -1,868 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
CoreObjectNameSingular,
FeatureFlagKey,
FieldMetadataType,
PageLayoutTabLayoutMode,
ViewType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
GRID_POSITIONS,
VERTICAL_LIST_LAYOUT_POSITIONS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
const HOME_TAB_POSITION = 10;
const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
const settings = field.settings as
| { junctionTargetFieldId?: string }
| undefined;
return (
isDefined(settings?.junctionTargetFieldId) &&
typeof settings?.junctionTargetFieldId === 'string' &&
settings.junctionTargetFieldId.length > 0
);
};
const isRelationTargetAvailable = (
targetObject: FlatObjectMetadata | undefined,
): boolean => {
if (!isDefined(targetObject)) {
return false;
}
if (targetObject.isRemote) {
return false;
}
if (
targetObject.isSystem &&
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
) {
return false;
}
return true;
};
@Command({
name: 'upgrade:1-20:backfill-page-layouts-and-fields-widget-view-fields',
description:
'Backfill RECORD_PAGE page layouts, sync FIELDS_WIDGET view fields, create FIELD widgets, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
})
export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts and field widgets for workspace ${workspaceId}`,
);
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId,
);
if (isAlreadyEnabled) {
this.logger.log(
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
);
return;
}
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create page layouts, sync view fields, create FIELD widgets and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
);
return;
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
await this.backfillStandardPageLayoutsAndSyncViews({
workspaceId,
twentyStandardFlatApplication,
});
await this.backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
});
await this.backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
});
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
workspaceId,
);
this.logger.log(
`Successfully backfilled page layouts and field widgets for workspace ${workspaceId}`,
);
}
private async backfillStandardPageLayoutsAndSyncViews({
workspaceId,
twentyStandardFlatApplication,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
}): Promise<void> {
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
shouldIncludeRecordPageLayouts: true,
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const recordPageLayoutUniversalIdentifiers = new Set<string>();
const recordPageLayoutsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((pageLayout) => {
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
return false;
}
recordPageLayoutUniversalIdentifiers.add(
pageLayout.universalIdentifier,
);
return true;
});
const tabUniversalIdentifiers = new Set<string>();
const tabsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((tab) => {
if (
!recordPageLayoutUniversalIdentifiers.has(
tab.pageLayoutUniversalIdentifier,
)
) {
return false;
}
tabUniversalIdentifiers.add(tab.universalIdentifier);
return true;
});
const widgetsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((widget) =>
tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier),
);
const {
flatPageLayoutMaps: existingFlatPageLayoutMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
]);
// Filter out page layouts, tabs, and widgets that already exist in the workspace
const pageLayoutsToCreate = recordPageLayoutsFromStandard.filter(
(pageLayout) =>
!isDefined(
existingFlatPageLayoutMaps.byUniversalIdentifier[
pageLayout.universalIdentifier
],
),
);
const pageLayoutTabsToCreate = tabsFromStandard.filter(
(tab) =>
!isDefined(
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
tab.universalIdentifier
],
),
);
const pageLayoutWidgetsToCreate = widgetsFromStandard.filter(
(widget) =>
!isDefined(
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
widget.universalIdentifier
],
),
);
// Collect all standard FIELDS_WIDGET view universal identifiers
// This includes both new views to create AND existing views (created by 1-19)
const allStandardFieldsWidgetViewUniversalIds = new Set<string>();
const viewsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((view) => {
if (view.type !== ViewType.FIELDS_WIDGET) {
return false;
}
allStandardFieldsWidgetViewUniversalIds.add(view.universalIdentifier);
if (
isDefined(
existingFlatViewMaps.byUniversalIdentifier[
view.universalIdentifier
],
)
) {
return false;
}
return true;
});
// Delete all existing viewFields for standard FIELDS_WIDGET views and recreate from current standard.
// This ensures positions, visibility, and groups match the current standard definition.
const viewFieldsToDelete = Object.values(
existingFlatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewField) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewField.viewUniversalIdentifier,
),
);
const viewFieldsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewField) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewField.viewUniversalIdentifier,
),
);
// Same delete-and-recreate approach for viewFieldGroups
const viewFieldGroupsToDelete = Object.values(
existingFlatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewFieldGroup) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewFieldGroup.viewUniversalIdentifier,
),
);
const viewFieldGroupsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewFieldGroup) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewFieldGroup.viewUniversalIdentifier,
),
);
this.logger.log(
`Creating ${pageLayoutsToCreate.length} page layouts, ${viewsToCreate.length} views, deleting ${viewFieldsToDelete.length} and creating ${viewFieldsToCreate.length} view fields, deleting ${viewFieldGroupsToDelete.length} and creating ${viewFieldGroupsToCreate.length} view field groups for workspace ${workspaceId}`,
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: pageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: pageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: pageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: viewFieldsToDelete,
flatEntityToUpdate: [],
},
viewFieldGroup: {
flatEntityToCreate: viewFieldGroupsToCreate,
flatEntityToDelete: viewFieldGroupsToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create standard page layouts and sync view fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create standard page layouts and sync view fields for workspace ${workspaceId}`,
);
}
}
private async backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
]);
const existingPageLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
).filter(isDefined);
const objectIdsWithRecordPageLayout = new Set(
existingPageLayouts
.filter(
(layout: FlatPageLayout) =>
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId),
)
.map((layout: FlatPageLayout) => layout.objectMetadataId),
);
const customObjectsWithoutPageLayout = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(objectMetadata) =>
objectMetadata.isCustom &&
!objectMetadata.isRemote &&
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
);
if (customObjectsWithoutPageLayout.length === 0) {
this.logger.log(
`No custom objects without page layouts found for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
for (const customObject of customObjectsWithoutPageLayout) {
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
});
const objectFieldMetadatas = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((field) => field.objectMetadataId === customObject.id);
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
flatApplication: workspaceCustomFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier: true,
});
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
recordPageFieldsView: flatRecordPageFieldsView,
workspaceId,
});
allCustomPageLayoutsToCreate.push(...pageLayouts);
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
allCustomViewsToCreate.push(flatRecordPageFieldsView);
allCustomViewFieldsToCreate.push(...viewFields);
}
const customValidateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: allCustomPageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: allCustomViewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: allCustomViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (customValidateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create custom object page layouts for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
}
private async backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
]);
// Build a set of fieldMetadataIds that already have a FIELD widget
const existingFieldWidgetFieldIds = new Set<string>();
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)) {
if (
isDefined(widget) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
isDefined(widget.configuration.fieldMetadataId)
) {
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
}
}
// Build object ID → objectMetadata map
const objectById = new Map<string, FlatObjectMetadata>();
for (const obj of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (isDefined(obj)) {
objectById.set(obj.id, obj);
}
}
// Build object ID → RECORD_PAGE layout map
const recordPageLayoutByObjectId = new Map<
string,
{ id: string; universalIdentifier: string }
>();
for (const layout of Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)) {
if (
isDefined(layout) &&
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId)
) {
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
id: layout.id,
universalIdentifier: layout.universalIdentifier,
});
}
}
// Build pageLayoutId → home tab map
const homeTabByPageLayoutId = new Map<
string,
{
id: string;
universalIdentifier: string;
widgetCount: number;
}
>();
for (const tab of Object.values(
flatPageLayoutTabMaps.byUniversalIdentifier,
)) {
if (
isDefined(tab) &&
tab.position === HOME_TAB_POSITION &&
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
) {
homeTabByPageLayoutId.set(tab.pageLayoutId, {
id: tab.id,
universalIdentifier: tab.universalIdentifier,
widgetCount: tab.widgetIds?.length ?? 0,
});
}
}
// Group fields by objectMetadataId
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
// Map morphId → all field IDs sharing that morphId (for dedup)
const fieldIdsByMorphId = new Map<string, string[]>();
for (const field of Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
continue;
}
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
list.push(field);
fieldsByObjectId.set(field.objectMetadataId, list);
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
morphFieldIds.push(field.id);
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
}
}
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
const pageLayout = recordPageLayoutByObjectId.get(object.id);
if (!isDefined(pageLayout)) {
continue;
}
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
if (!isDefined(homeTab)) {
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
for (const field of fields) {
if (
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION
) {
continue;
}
if (isActivityTargetField(field.name, object.nameSingular)) {
continue;
}
if (isJunctionRelationField(field)) {
continue;
}
if (field.type === FieldMetadataType.RELATION) {
const targetObject = isDefined(field.relationTargetObjectMetadataId)
? objectById.get(field.relationTargetObjectMetadataId)
: undefined;
if (!isRelationTargetAvailable(targetObject)) {
continue;
}
}
// For morph relations, skip if any sibling field (same morphId)
// already has a widget or was already processed in this run
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
if (processedMorphIds.has(field.morphId)) {
continue;
}
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
const alreadyHasWidget = siblingFieldIds.some((id) =>
existingFieldWidgetFieldIds.has(id),
);
if (alreadyHasWidget) {
continue;
}
processedMorphIds.add(field.morphId);
}
if (existingFieldWidgetFieldIds.has(field.id)) {
continue;
}
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
title: field.label,
type: WidgetType.FIELD,
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
position: {
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
index: nextWidgetIndex,
},
configuration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
universalConfiguration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.universalIdentifier,
fieldDisplayMode: FieldDisplayMode.CARD,
},
objectMetadataId: object.id,
objectMetadataUniversalIdentifier: object.universalIdentifier,
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
overrides: null,
};
targetList.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
);
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,485 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { FeatureFlagKey, ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
@Command({
name: 'upgrade:1-20:backfill-page-layouts',
description:
'Backfill RECORD_PAGE page layouts for legacy workspaces and enable the IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED feature flag',
})
export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts for workspace ${workspaceId}`,
);
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId,
);
if (isAlreadyEnabled) {
this.logger.log(
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
);
return;
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
await this.backfillStandardObjectPageLayouts({
workspaceId,
twentyStandardFlatApplication,
isDryRun,
});
await this.backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
isDryRun,
});
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
workspaceId,
);
}
this.logger.log(
isDryRun
? `[DRY RUN] Would create page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`
: `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
);
}
private async backfillStandardObjectPageLayouts({
workspaceId,
twentyStandardFlatApplication,
isDryRun,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
isDryRun: boolean;
}): Promise<void> {
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
shouldIncludeRecordPageLayouts: true,
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const {
flatPageLayoutMaps: existingFlatPageLayoutMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
]);
const recordPageLayoutUniversalIdentifiers = new Set<string>();
const pageLayoutsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((pageLayout) => {
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
return false;
}
recordPageLayoutUniversalIdentifiers.add(
pageLayout.universalIdentifier,
);
if (
isDefined(
existingFlatPageLayoutMaps.byUniversalIdentifier[
pageLayout.universalIdentifier
],
)
) {
return false;
}
return true;
});
const tabUniversalIdentifiers = new Set<string>();
const pageLayoutTabsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((tab) => {
if (
!recordPageLayoutUniversalIdentifiers.has(
tab.pageLayoutUniversalIdentifier,
)
) {
return false;
}
tabUniversalIdentifiers.add(tab.universalIdentifier);
if (
isDefined(
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
tab.universalIdentifier
],
)
) {
return false;
}
return true;
});
const pageLayoutWidgetsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
tabUniversalIdentifiers.has(
widget.pageLayoutTabUniversalIdentifier,
) &&
!isDefined(
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
widget.universalIdentifier
],
),
);
const viewUniversalIdentifiers = new Set<string>();
const viewsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((view) => {
if (view.type !== ViewType.FIELDS_WIDGET) {
return false;
}
if (
isDefined(
existingFlatViewMaps.byUniversalIdentifier[
view.universalIdentifier
],
)
) {
return false;
}
viewUniversalIdentifiers.add(view.universalIdentifier);
return true;
});
const viewFieldsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewField) =>
viewUniversalIdentifiers.has(viewField.viewUniversalIdentifier) &&
!isDefined(
existingFlatViewFieldMaps.byUniversalIdentifier[
viewField.universalIdentifier
],
),
);
const viewFieldGroupsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewFieldGroup) =>
viewUniversalIdentifiers.has(
viewFieldGroup.viewUniversalIdentifier,
) &&
!isDefined(
existingFlatViewFieldGroupMaps.byUniversalIdentifier[
viewFieldGroup.universalIdentifier
],
),
);
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Found standard entities to create for workspace ${workspaceId}: ` +
`${pageLayoutsToCreate.length} page layout(s), ` +
`${pageLayoutTabsToCreate.length} tab(s), ` +
`${pageLayoutWidgetsToCreate.length} widget(s), ` +
`${viewsToCreate.length} view(s), ` +
`${viewFieldsToCreate.length} view field(s), ` +
`${viewFieldGroupsToCreate.length} view field group(s)`,
);
if (isDryRun) {
return;
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: pageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: pageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: pageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewFieldGroup: {
flatEntityToCreate: viewFieldGroupsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create standard page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create standard page layouts for workspace ${workspaceId}`,
);
}
}
private async backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
isDryRun,
}: {
workspaceId: string;
workspaceCustomFlatApplication: FlatApplication;
isDryRun: boolean;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
]);
const existingPageLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
).filter(isDefined);
const objectIdsWithRecordPageLayout = new Set(
existingPageLayouts
.filter(
(layout: FlatPageLayout) =>
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId),
)
.map((layout: FlatPageLayout) => layout.objectMetadataId),
);
const customObjectsWithoutPageLayout = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(objectMetadata) =>
objectMetadata.isCustom &&
!objectMetadata.isRemote &&
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
);
if (customObjectsWithoutPageLayout.length === 0) {
this.logger.log(
`No custom objects without page layouts found for workspace ${workspaceId}`,
);
return;
}
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
for (const customObject of customObjectsWithoutPageLayout) {
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
});
const objectFieldMetadatas = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((field) => field.objectMetadataId === customObject.id);
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
flatApplication: workspaceCustomFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
});
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
recordPageFieldsView: flatRecordPageFieldsView,
workspaceId,
});
allCustomPageLayoutsToCreate.push(...pageLayouts);
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
allCustomViewsToCreate.push(flatRecordPageFieldsView);
allCustomViewFieldsToCreate.push(...viewFields);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Found custom entities to create for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}: ` +
`${allCustomPageLayoutsToCreate.length} page layout(s), ` +
`${allCustomPageLayoutTabsToCreate.length} tab(s), ` +
`${allCustomPageLayoutWidgetsToCreate.length} widget(s), ` +
`${allCustomViewsToCreate.length} view(s), ` +
`${allCustomViewFieldsToCreate.length} view field(s)`,
);
if (isDryRun) {
return;
}
const customValidateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: allCustomPageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: allCustomViewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: allCustomViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (customValidateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create custom object page layouts for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,67 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-20:generate-application-sdk-clients',
description:
'Generate SDK client archives for all existing applications so drivers do not crash with ARCHIVE_NOT_FOUND',
})
export class GenerateApplicationSdkClientsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly sdkClientGenerationService: SdkClientGenerationService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const dryRun = options.dryRun ?? false;
const applications = await this.applicationRepository.find({
where: { workspaceId },
});
this.logger.log(
`Found ${applications.length} application(s) in workspace ${workspaceId}`,
);
for (const application of applications) {
if (dryRun) {
this.logger.log(
`[DRY RUN] Would generate SDK client for application ${application.universalIdentifier} (${application.id})`,
);
continue;
}
try {
await this.sdkClientGenerationService.generateSdkClientForApplication({
workspaceId,
applicationId: application.id,
applicationUniversalIdentifier: application.universalIdentifier,
});
} catch (error) {
this.logger.error(
`Failed to generate SDK client for application ${application.universalIdentifier} (${application.id}): ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
}
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -16,8 +18,10 @@ import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-vers
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
@@ -37,6 +41,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
imports: [
TypeOrmModule.forFeature([
WorkspaceEntity,
ApplicationEntity,
ConnectedAccountEntity,
MessageChannelEntity,
CalendarChannelEntity,
@@ -53,6 +58,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ApplicationRegistrationModule,
WorkspaceMigrationModule,
FeatureFlagModule,
SdkClientModule,
WorkflowCommonModule,
],
providers: [
@@ -61,10 +67,12 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillPageLayoutsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -77,10 +85,12 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillPageLayoutsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -34,10 +34,11 @@ import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgra
import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command';
import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command';
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -45,6 +46,7 @@ import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrati
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -104,10 +106,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
protected readonly generateApplicationSdkClientsCommand: GenerateApplicationSdkClientsCommand,
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
protected readonly backfillFieldWidgetsCommand: BackfillFieldWidgetsCommand,
protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand,
protected readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand,
protected readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand,
@@ -168,12 +172,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migrateRichTextToTextCommand,
this.deleteOrphanNavigationMenuItemsCommand,
this.backfillCommandMenuItemsCommand,
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
this.backfillPageLayoutsCommand,
this.seedCliApplicationRegistrationCommand,
this.migrateMessagingInfrastructureToMetadataCommand,
this.backfillFieldWidgetsCommand,
this.backfillSelectFieldOptionIdsCommand,
this.updateStandardIndexViewNamesCommand,
this.makeWorkflowSearchableCommand,
this.generateApplicationSdkClientsCommand,
];
this.allCommands = {
@@ -721,7 +721,6 @@ export class LambdaDriver implements LogicFunctionDriver {
const sdkArchiveBuffer =
await this.sdkClientArchiveService.downloadArchiveBuffer({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
});
@@ -113,7 +113,6 @@ export class LocalDriver implements LogicFunctionDriver {
await this.sdkClientArchiveService.downloadAndExtractToPackage({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
targetPackagePath: sdkPackagePath,
});
@@ -59,7 +59,6 @@ export class SdkClientController {
const fileBuffer =
await this.sdkClientArchiveService.getClientModuleFromArchive({
workspaceId: workspace.id,
applicationId,
applicationUniversalIdentifier: application.universalIdentifier,
moduleName,
});
@@ -1,7 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createWriteStream } from 'fs';
import * as fs from 'fs/promises';
import { join } from 'path';
import { type Readable } from 'stream';
import { pipeline } from 'stream/promises';
import { FileFolder } from 'twenty-shared/types';
import { Repository } from 'typeorm';
@@ -12,12 +16,12 @@ import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules';
import {
SdkClientException,
SdkClientExceptionCode,
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -25,74 +29,75 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
@Injectable()
export class SdkClientArchiveService {
private readonly logger = new Logger(SdkClientArchiveService.name);
constructor(
private readonly fileStorageService: FileStorageService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly sdkClientGenerationService: SdkClientGenerationService,
) {}
async downloadAndExtractToPackage({
workspaceId,
applicationId,
applicationUniversalIdentifier,
targetPackagePath,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
targetPackagePath: string;
}): Promise<void> {
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
const temporaryDirManager = new TemporaryDirManager();
await fs.rm(targetPackagePath, { recursive: true, force: true });
await fs.mkdir(targetPackagePath, { recursive: true });
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME);
const { default: unzipper } = await import('unzipper');
const directory = await unzipper.Open.buffer(archiveBuffer);
const archiveStream = await this.readArchiveStream({
workspaceId,
applicationUniversalIdentifier,
});
await directory.extract({ path: targetPackagePath });
await pipeline(archiveStream, createWriteStream(archivePath));
await fs.rm(targetPackagePath, { recursive: true, force: true });
await fs.mkdir(targetPackagePath, { recursive: true });
const { default: unzipper } = await import('unzipper');
const directory = await unzipper.Open.file(archivePath);
await directory.extract({ path: targetPackagePath });
} finally {
await temporaryDirManager.clean();
}
}
async downloadArchiveBuffer({
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<Buffer> {
return this.downloadArchiveBufferOrGenerate({
const archiveStream = await this.readArchiveStream({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
return streamToBuffer(archiveStream);
}
async getClientModuleFromArchive({
workspaceId,
applicationId,
applicationUniversalIdentifier,
moduleName,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
moduleName: SdkModuleName;
}): Promise<Buffer> {
const filePath = `dist/${moduleName}.mjs`;
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
const archiveBuffer = await this.downloadArchiveBuffer({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
@@ -130,41 +135,32 @@ export class SdkClientArchiveService {
]);
}
private async downloadArchiveBufferOrGenerate({
private async readArchiveStream({
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<Buffer> {
}): Promise<Readable> {
try {
const stream = await this.fileStorageService.readFile({
return await this.fileStorageService.readFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.GeneratedSdkClient,
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
});
return await streamToBuffer(stream);
} catch (error) {
if (
!(error instanceof FileStorageException) ||
error.code !== FileStorageExceptionCode.FILE_NOT_FOUND
error instanceof FileStorageException &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
throw error;
throw new SdkClientException(
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
);
}
throw error;
}
this.logger.warn(
`SDK client archive missing for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}", generating on-the-fly`,
);
return this.sdkClientGenerationService.generateSdkClientForApplication({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
}
}
@@ -44,13 +44,13 @@ export class SdkClientGenerationService {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<Buffer> {
}): Promise<void> {
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
{ id: workspaceId } as WorkspaceEntity,
applicationId,
);
const archiveBuffer = await this.generateAndStore({
await this.generateAndStore({
workspaceId,
applicationId,
applicationUniversalIdentifier,
@@ -60,8 +60,6 @@ export class SdkClientGenerationService {
this.logger.log(
`Generated SDK client for application ${applicationUniversalIdentifier}`,
);
return archiveBuffer;
}
private async generateAndStore({
@@ -74,7 +72,7 @@ export class SdkClientGenerationService {
applicationId: string;
applicationUniversalIdentifier: string;
schema: string;
}): Promise<Buffer> {
}): Promise<void> {
const temporaryDirManager = new TemporaryDirManager();
try {
@@ -103,14 +101,12 @@ export class SdkClientGenerationService {
await createZipFile(tempPackageRoot, archivePath);
const archiveBuffer = await fs.readFile(archivePath);
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.GeneratedSdkClient,
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
sourceFile: archiveBuffer,
sourceFile: await fs.readFile(archivePath),
mimeType: 'application/zip',
settings: { isTemporaryFile: false, toDelete: false },
});
@@ -123,8 +119,6 @@ export class SdkClientGenerationService {
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatApplicationMaps',
]);
return archiveBuffer;
} catch (error) {
throw new SdkClientException(
`Failed to generate SDK client for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}": ${error instanceof Error ? error.message : String(error)}`,
File diff suppressed because it is too large Load Diff
@@ -455,7 +455,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
flatDefaultRecordPageLayoutsToCreate =
@@ -1,209 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { computeFlatViewFieldsToCreate } from '../compute-flat-view-fields-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('computeFlatViewFieldsToCreate', () => {
it('should exclude TS_VECTOR fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude POSITION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'position',
type: FieldMetadataType.POSITION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude RELATION and MORPH_RELATION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'company',
type: FieldMetadataType.RELATION,
}),
makeFieldMetadata({
name: 'target',
type: FieldMetadataType.MORPH_RELATION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude deletedAt field', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should place label identifier field first', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const fields = [otherField, labelField];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
});
expect(result).toHaveLength(2);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
labelField.universalIdentifier,
);
expect(result[0].position).toBe(0);
expect(result[1].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
expect(result[1].position).toBe(1);
});
it('should exclude label identifier when excludeLabelIdentifier is true', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [labelField, otherField],
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
excludeLabelIdentifier: true,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
});
});
@@ -11,30 +11,22 @@ export const computeFlatViewFieldsToCreate = ({
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier = false,
}: {
flatApplication: FlatApplication;
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
excludeLabelIdentifier?: boolean;
}): UniversalFlatViewField[] => {
const createdAt = new Date().toISOString();
const defaultViewFields = objectFlatFieldMetadatas
.filter(
(field) =>
field.name !== 'deletedAt' &&
field.type !== FieldMetadataType.TS_VECTOR &&
field.type !== FieldMetadataType.POSITION &&
field.type !== FieldMetadataType.MORPH_RELATION &&
field.type !== FieldMetadataType.RELATION &&
// Include 'id' only if it's the label identifier (e.g., for junction tables)
(field.name !== 'id' ||
field.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier) &&
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
(!excludeLabelIdentifier ||
field.universalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier),
)
.sort((a, b) => {
@@ -6,37 +6,40 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
"id": "00000000-0000-0000-0000-000000000011",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000026",
"id": "00000000-0000-0000-0000-000000000027",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000027",
"id": "00000000-0000-0000-0000-000000000028",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000024",
"id": "00000000-0000-0000-0000-000000000025",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000025",
"id": "00000000-0000-0000-0000-000000000026",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000022",
"id": "00000000-0000-0000-0000-000000000023",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000023",
"id": "00000000-0000-0000-0000-000000000024",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000012",
"widgets": {
"accountOwner": {
"id": "00000000-0000-0000-0000-000000000015",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000013",
},
"opportunities": {
"id": "00000000-0000-0000-0000-000000000015",
"id": "00000000-0000-0000-0000-000000000016",
},
"people": {
"id": "00000000-0000-0000-0000-000000000014",
@@ -44,26 +47,26 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000020",
"id": "00000000-0000-0000-0000-000000000021",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000021",
"id": "00000000-0000-0000-0000-000000000022",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000018",
"id": "00000000-0000-0000-0000-000000000019",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000019",
"id": "00000000-0000-0000-0000-000000000020",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000016",
"id": "00000000-0000-0000-0000-000000000017",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000017",
"id": "00000000-0000-0000-0000-000000000018",
},
},
},
@@ -104,278 +107,281 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"noteRecordPage": {
"id": "00000000-0000-0000-0000-000000000063",
"id": "00000000-0000-0000-0000-000000000064",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000071",
"id": "00000000-0000-0000-0000-000000000072",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000072",
"id": "00000000-0000-0000-0000-000000000073",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000064",
"id": "00000000-0000-0000-0000-000000000065",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000065",
"id": "00000000-0000-0000-0000-000000000066",
},
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000066",
"id": "00000000-0000-0000-0000-000000000067",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000067",
"id": "00000000-0000-0000-0000-000000000068",
"widgets": {
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000068",
"id": "00000000-0000-0000-0000-000000000069",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000069",
"id": "00000000-0000-0000-0000-000000000070",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000070",
"id": "00000000-0000-0000-0000-000000000071",
},
},
},
},
},
"opportunityRecordPage": {
"id": "00000000-0000-0000-0000-000000000045",
"id": "00000000-0000-0000-0000-000000000046",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000061",
"id": "00000000-0000-0000-0000-000000000062",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000062",
"id": "00000000-0000-0000-0000-000000000063",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000059",
"id": "00000000-0000-0000-0000-000000000060",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000060",
"id": "00000000-0000-0000-0000-000000000061",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000057",
"id": "00000000-0000-0000-0000-000000000058",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000058",
"id": "00000000-0000-0000-0000-000000000059",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000046",
"id": "00000000-0000-0000-0000-000000000047",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000049",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000047",
},
"owner": {
"id": "00000000-0000-0000-0000-000000000050",
},
"pointOfContact": {
"fields": {
"id": "00000000-0000-0000-0000-000000000048",
},
"owner": {
"id": "00000000-0000-0000-0000-000000000051",
},
"pointOfContact": {
"id": "00000000-0000-0000-0000-000000000049",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000055",
"id": "00000000-0000-0000-0000-000000000056",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000056",
"id": "00000000-0000-0000-0000-000000000057",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000053",
"id": "00000000-0000-0000-0000-000000000054",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000054",
"id": "00000000-0000-0000-0000-000000000055",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000051",
"id": "00000000-0000-0000-0000-000000000052",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000052",
"id": "00000000-0000-0000-0000-000000000053",
},
},
},
},
},
"personRecordPage": {
"id": "00000000-0000-0000-0000-000000000028",
"id": "00000000-0000-0000-0000-000000000029",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000043",
"id": "00000000-0000-0000-0000-000000000044",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000044",
"id": "00000000-0000-0000-0000-000000000045",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000041",
"id": "00000000-0000-0000-0000-000000000042",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000042",
"id": "00000000-0000-0000-0000-000000000043",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000039",
"id": "00000000-0000-0000-0000-000000000040",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000040",
"id": "00000000-0000-0000-0000-000000000041",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000029",
"id": "00000000-0000-0000-0000-000000000030",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000031",
"id": "00000000-0000-0000-0000-000000000032",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000030",
"id": "00000000-0000-0000-0000-000000000031",
},
"pointOfContactForOpportunities": {
"id": "00000000-0000-0000-0000-000000000032",
"id": "00000000-0000-0000-0000-000000000033",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000037",
"id": "00000000-0000-0000-0000-000000000038",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000038",
"id": "00000000-0000-0000-0000-000000000039",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000035",
"id": "00000000-0000-0000-0000-000000000036",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000036",
"id": "00000000-0000-0000-0000-000000000037",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000033",
"id": "00000000-0000-0000-0000-000000000034",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000034",
"id": "00000000-0000-0000-0000-000000000035",
},
},
},
},
},
"taskRecordPage": {
"id": "00000000-0000-0000-0000-000000000073",
"id": "00000000-0000-0000-0000-000000000074",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000081",
"id": "00000000-0000-0000-0000-000000000083",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000082",
"id": "00000000-0000-0000-0000-000000000084",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000074",
"id": "00000000-0000-0000-0000-000000000075",
"widgets": {
"assignee": {
"id": "00000000-0000-0000-0000-000000000078",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000075",
"id": "00000000-0000-0000-0000-000000000076",
},
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000076",
"id": "00000000-0000-0000-0000-000000000077",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000077",
"id": "00000000-0000-0000-0000-000000000079",
"widgets": {
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000078",
"id": "00000000-0000-0000-0000-000000000080",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000079",
"id": "00000000-0000-0000-0000-000000000081",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000080",
"id": "00000000-0000-0000-0000-000000000082",
},
},
},
},
},
"workflowRecordPage": {
"id": "00000000-0000-0000-0000-000000000083",
"id": "00000000-0000-0000-0000-000000000085",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000084",
"id": "00000000-0000-0000-0000-000000000086",
"widgets": {
"workflow": {
"id": "00000000-0000-0000-0000-000000000085",
"id": "00000000-0000-0000-0000-000000000087",
},
},
},
},
},
"workflowRunRecordPage": {
"id": "00000000-0000-0000-0000-000000000092",
"id": "00000000-0000-0000-0000-000000000094",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000096",
"id": "00000000-0000-0000-0000-000000000098",
"widgets": {
"workflowRun": {
"id": "00000000-0000-0000-0000-000000000097",
"id": "00000000-0000-0000-0000-000000000099",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000093",
"id": "00000000-0000-0000-0000-000000000095",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000094",
"id": "00000000-0000-0000-0000-000000000096",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000095",
"id": "00000000-0000-0000-0000-000000000097",
},
},
},
},
},
"workflowVersionRecordPage": {
"id": "00000000-0000-0000-0000-000000000086",
"id": "00000000-0000-0000-0000-000000000088",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000090",
"id": "00000000-0000-0000-0000-000000000092",
"widgets": {
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000091",
"id": "00000000-0000-0000-0000-000000000093",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000087",
"id": "00000000-0000-0000-0000-000000000089",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000088",
"id": "00000000-0000-0000-0000-000000000090",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000089",
"id": "00000000-0000-0000-0000-000000000091",
},
},
},
@@ -31,12 +31,21 @@ const COMPANY_PAGE_TABS = {
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.people.universalIdentifier,
},
accountOwner: {
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0113',
title: 'Account Owner',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.accountOwner.universalIdentifier,
},
opportunities: {
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0114',
title: 'Opportunities',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FOURTH,
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.opportunities.universalIdentifier,
},
@@ -1,10 +1,13 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
CONDITIONAL_DISPLAY_DEVICE_MOBILE,
GRID_POSITIONS,
TAB_PROPS,
VERTICAL_LIST_LAYOUT_POSITIONS,
WIDGET_PROPS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import {
@@ -29,6 +32,15 @@ const TASK_PAGE_TABS = {
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_MOBILE,
},
assignee: {
universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5513',
title: 'Assignee',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.task.fields.assignee.universalIdentifier,
},
},
},
note: {
@@ -45,6 +45,19 @@ export const computeStandardBlocklistViewFields = (
},
}),
blocklistRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'blocklist',
context: {
viewName: 'blocklistRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
blocklistRecordPageFieldsWorkspaceMember:
createStandardViewFieldFlatMetadata({
...args,
@@ -81,6 +81,19 @@ export const computeStandardCalendarChannelViewFields = (
},
}),
calendarChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'calendarChannel',
context: {
viewName: 'calendarChannelRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
calendarChannelRecordPageFieldsConnectedAccount:
createStandardViewFieldFlatMetadata({
...args,
@@ -245,123 +245,5 @@ export const computeStandardCompanyViewFields = (
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsPeople: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'people',
fieldName: 'people',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsOpportunities: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'opportunities',
fieldName: 'opportunities',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -69,6 +69,21 @@ export const computeStandardConnectedAccountViewFields = (
},
}),
connectedAccountRecordPageFieldsHandle: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'connectedAccount',
context: {
viewName: 'connectedAccountRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
},
),
connectedAccountRecordPageFieldsProvider:
createStandardViewFieldFlatMetadata({
...args,
@@ -33,6 +33,19 @@ export const computeStandardFavoriteFolderViewFields = (
},
}),
favoriteFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'favoriteFolder',
context: {
viewName: 'favoriteFolderRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
favoriteFolderRecordPageFieldsCreatedAt:
createStandardViewFieldFlatMetadata({
...args,
@@ -93,6 +93,19 @@ export const computeStandardMessageChannelViewFields = (
},
}),
messageChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageChannel',
context: {
viewName: 'messageChannelRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageChannelRecordPageFieldsConnectedAccount:
createStandardViewFieldFlatMetadata({
...args,
@@ -69,6 +69,19 @@ export const computeStandardMessageFolderViewFields = (
},
}),
messageFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageFolder',
context: {
viewName: 'messageFolderRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageFolderRecordPageFieldsMessageChannel:
createStandardViewFieldFlatMetadata({
...args,
@@ -93,6 +93,20 @@ export const computeStandardMessageParticipantViewFields = (
},
}),
messageParticipantRecordPageFieldsHandle:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageParticipant',
context: {
viewName: 'messageParticipantRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageParticipantRecordPageFieldsMessage:
createStandardViewFieldFlatMetadata({
...args,
@@ -70,6 +70,19 @@ export const computeStandardNoteViewFields = (
}),
// noteRecordPageFields view fields
noteRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
noteRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
@@ -109,85 +122,5 @@ export const computeStandardNoteViewFields = (
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'bodyV2',
fieldName: 'bodyV2',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
noteRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
noteRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
noteRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -266,103 +266,5 @@ export const computeStandardOpportunityViewFields = (
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
opportunityRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -266,152 +266,5 @@ export const computeStandardPersonViewFields = (
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsAvatarFile: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'avatarFile',
fieldName: 'avatarFile',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
personRecordPageFieldsPointOfContactForOpportunities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'pointOfContactForOpportunities',
fieldName: 'pointOfContactForOpportunities',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsMessageParticipants:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'messageParticipants',
fieldName: 'messageParticipants',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsCalendarEventParticipants:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'calendarEventParticipants',
fieldName: 'calendarEventParticipants',
position: 11,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 12,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -255,6 +255,19 @@ export const computeStandardTaskViewFields = (
}),
// taskRecordPageFields view fields
taskRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
taskRecordPageFieldsDueAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
@@ -333,85 +346,5 @@ export const computeStandardTaskViewFields = (
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'bodyV2',
fieldName: 'bodyV2',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
taskRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
taskRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
taskRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -46,6 +46,19 @@ export const computeStandardWorkflowRunViewFields = (
}),
// workflowRunRecordPageFields view fields
workflowRunRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
@@ -138,110 +151,5 @@ export const computeStandardWorkflowRunViewFields = (
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsEnqueuedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'enqueuedAt',
fieldName: 'enqueuedAt',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
workflowRunRecordPageFieldsOutput: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'output',
fieldName: 'output',
position: 6,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsContext: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'context',
fieldName: 'context',
position: 7,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsState: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'state',
fieldName: 'state',
position: 8,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -70,6 +70,19 @@ export const computeStandardWorkflowVersionViewFields = (
}),
// workflowVersionRecordPageFields view fields
workflowVersionRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
@@ -126,101 +139,5 @@ export const computeStandardWorkflowVersionViewFields = (
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsSteps: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'steps',
fieldName: 'steps',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
workflowVersionRecordPageFieldsCreatedBy:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'createdBy',
fieldName: 'createdBy',
position: 1,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsUpdatedAt:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsUpdatedBy:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsRuns: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'runs',
fieldName: 'runs',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsFavorites:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -4,4 +4,5 @@ export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
] as const satisfies FeatureFlagKey[];
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
@@ -96,9 +95,8 @@ export class FlatViewFieldValidatorService {
}
if (
flatView.type !== ViewType.FIELDS_WIDGET &&
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
updatedFlatViewField.fieldMetadataUniversalIdentifier
updatedFlatViewField.fieldMetadataUniversalIdentifier
) {
const otherFlatViewFields =
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow(
@@ -129,7 +127,6 @@ export class FlatViewFieldValidatorService {
flatViewFieldMaps: optimisticFlatViewFieldMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatViewMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.viewField
@@ -180,18 +177,11 @@ export class FlatViewFieldValidatorService {
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
existingFlatViewField.fieldMetadataUniversalIdentifier
) {
const flatView = findFlatEntityByUniversalIdentifier({
universalIdentifier: existingFlatViewField.viewUniversalIdentifier,
flatEntityMaps: flatViewMaps,
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
});
if (!isDefined(flatView) || flatView.type !== ViewType.FIELDS_WIDGET) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
});
}
}
return validationResult;
@@ -299,9 +289,8 @@ export class FlatViewFieldValidatorService {
}
if (
flatView.type !== ViewType.FIELDS_WIDGET &&
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
) {
validationResult.errors.push(
...validateLabelIdentifierFieldMetadataIdFlatViewField({
@@ -310,7 +299,6 @@ export class FlatViewFieldValidatorService {
}),
);
} else if (
flatView.type !== ViewType.FIELDS_WIDGET &&
otherFlatViewFields.some(
(flatViewField) =>
flatViewField.fieldMetadataUniversalIdentifier ===
@@ -93,6 +93,6 @@ describe('View side effect on object creation', () => {
expectToFail: false,
});
expect(createdViewFields.length).toBe(5);
expect(createdViewFields.length).toBe(7);
});
});
@@ -188,6 +188,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'e22a1d19-c1bb-4265-ae48-2054513c21fe',
},
workspaceMember: {
universalIdentifier: 'f2f5732f-7435-44be-986b-4c4d834fdfeb',
},
@@ -402,6 +405,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'cd1f641b-5746-49db-9a7e-82dd9a63593d',
},
connectedAccount: {
universalIdentifier: 'bdb40f41-f9ba-4b59-a8cf-878c23701ab3',
},
@@ -802,33 +808,6 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1210',
},
updatedAt: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1212',
},
updatedBy: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1213',
},
people: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1214',
},
taskTargets: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1215',
},
noteTargets: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1216',
},
opportunities: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1217',
},
favorites: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1218',
},
attachments: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1219',
},
timelineActivities: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c121a',
},
},
},
},
@@ -935,6 +914,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: '41d86e23-ceb2-41ab-975f-8ec5a1023ece',
},
provider: {
universalIdentifier: '83171d2a-0d11-42b1-991d-8d4346b02cff',
},
@@ -1231,6 +1213,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
name: {
universalIdentifier: 'cac4f0f7-3a6a-49b1-b86f-41b20f2455c0',
},
createdAt: {
universalIdentifier: 'a4e42591-844c-47d1-b72e-5ded3d541694',
},
@@ -1569,6 +1554,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'a9cbb9a5-a6b4-417e-93ad-a5e578c222db',
},
connectedAccount: {
universalIdentifier: '19079cf6-2a9c-40b9-b6c2-58d63c6e37ad',
},
@@ -1682,6 +1670,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
name: {
universalIdentifier: '6fa8c474-ee22-47f1-b830-4f169ff82315',
},
messageChannel: {
universalIdentifier: '2fb6ff09-bed5-4b31-af0f-7fa3df5612da',
},
@@ -1800,6 +1791,9 @@ export const STANDARD_OBJECTS = {
role: {
universalIdentifier: '5d1f9a65-85cc-41b2-a8bf-8e2c97aab4b3',
},
handle: {
universalIdentifier: '97295fc0-cdb8-4d84-8c1b-327837255c0d',
},
displayName: {
universalIdentifier: 'c50748fe-9f54-4e09-b572-111f076ec7db',
},
@@ -2024,6 +2018,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
title: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115201',
},
createdAt: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115202',
},
@@ -2033,24 +2030,6 @@ export const STANDARD_OBJECTS = {
noteTargets: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115204',
},
bodyV2: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115205',
},
updatedAt: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115206',
},
updatedBy: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115207',
},
attachments: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115208',
},
timelineActivities: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115209',
},
favorites: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a11520a',
},
},
},
},
@@ -2299,27 +2278,6 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca3208',
},
updatedAt: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320a',
},
updatedBy: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320b',
},
favorites: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320c',
},
taskTargets: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320d',
},
noteTargets: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320e',
},
attachments: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320f',
},
timelineActivities: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca3210',
},
},
},
},
@@ -2479,39 +2437,6 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12210',
},
updatedAt: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12212',
},
updatedBy: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12213',
},
avatarFile: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12214',
},
pointOfContactForOpportunities: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12215',
},
taskTargets: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12216',
},
noteTargets: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12217',
},
favorites: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12218',
},
attachments: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12219',
},
messageParticipants: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221a',
},
calendarEventParticipants: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221b',
},
timelineActivities: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221c',
},
},
},
},
@@ -2685,6 +2610,9 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
title: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6201',
},
dueAt: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6202',
},
@@ -2703,24 +2631,6 @@ export const STANDARD_OBJECTS = {
taskTargets: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6207',
},
bodyV2: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6208',
},
updatedAt: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6209',
},
updatedBy: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620a',
},
attachments: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620b',
},
timelineActivities: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620c',
},
favorites: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620d',
},
},
},
},
@@ -3204,6 +3114,9 @@ export const STANDARD_OBJECTS = {
workflowRunRecordPageFields: {
universalIdentifier: '20202020-a011-4a11-8a11-a0bcf10abcf1',
viewFields: {
name: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcf5',
},
status: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcf6',
},
@@ -3225,30 +3138,6 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfc',
},
enqueuedAt: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfd',
},
output: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfe',
},
context: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcff',
},
state: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd01',
},
updatedAt: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd02',
},
updatedBy: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd03',
},
favorites: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd04',
},
timelineActivities: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd05',
},
},
viewFieldGroups: {
general: {
@@ -3340,6 +3229,9 @@ export const STANDARD_OBJECTS = {
workflowVersionRecordPageFields: {
universalIdentifier: '20202020-a010-4a10-8a10-a0bcf10aaef1',
viewFields: {
name: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef5',
},
status: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef6',
},
@@ -3352,27 +3244,6 @@ export const STANDARD_OBJECTS = {
createdAt: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef9',
},
steps: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefa',
},
createdBy: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefb',
},
updatedAt: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefc',
},
updatedBy: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefd',
},
runs: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefe',
},
favorites: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaeff',
},
timelineActivities: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaf01',
},
},
viewFieldGroups: {
general: {