Compare commits

...
Author SHA1 Message Date
Weiko 1334e1707d Merge branch 'main' into c--reset-page-layout-tab 2026-04-07 15:06:30 +02:00
Weiko e87285d223 reset tab 2026-04-07 11:15:27 +02:00
Weiko e2dae88a3d Reset page layout tabs 2026-04-07 11:15:14 +02:00
30 changed files with 1401 additions and 152 deletions
File diff suppressed because one or more lines are too long
@@ -94,6 +94,20 @@ export const PageLayoutInitializationQueryEffect = ({
useEffect(() => {
if (!pageLayoutIsInitialized && isDefined(pageLayout)) {
const currentPersisted = store.get(
pageLayoutPersistedComponentCallbackState,
);
// After a reset, pageLayoutIsInitialized is set to false while the
// metadata store refetches. Skip re-initialization when the data
// hasn't actually changed yet to avoid overwriting with stale data.
if (
isDefined(currentPersisted) &&
isDeeplyEqual(pageLayout, currentPersisted)
) {
return;
}
initializePageLayout(pageLayout);
setPageLayoutIsInitialized(true);
}
@@ -102,6 +116,8 @@ export const PageLayoutInitializationQueryEffect = ({
pageLayoutIsInitialized,
pageLayout,
setPageLayoutIsInitialized,
store,
pageLayoutPersistedComponentCallbackState,
]);
return null;
@@ -0,0 +1,11 @@
import { PAGE_LAYOUT_TAB_FRAGMENT } from '@/dashboards/graphql/fragments/pageLayoutTabFragment';
import { gql } from '@apollo/client';
export const RESET_PAGE_LAYOUT_TAB_TO_DEFAULT = gql`
${PAGE_LAYOUT_TAB_FRAGMENT}
mutation ResetPageLayoutTabToDefault($id: String!) {
resetPageLayoutTabToDefault(id: $id) {
...PageLayoutTabFragment
}
}
`;
@@ -0,0 +1,138 @@
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CrudOperationType } from 'twenty-shared/types';
import { ResetPageLayoutTabToDefaultDocument } from '~/generated-metadata/graphql';
export const useResetPageLayoutTabToDefault = (
pageLayoutIdFromProps?: string,
) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const [resetMutation] = useMutation(ResetPageLayoutTabToDefaultDocument);
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { invalidateMetadataStore } = useInvalidateMetadataStore();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const store = useStore();
const hasInitializedState = useAtomComponentStateCallbackState(
hasInitializedFieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const groupsDraftState = useAtomComponentStateCallbackState(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const groupsPersistedState = useAtomComponentStateCallbackState(
fieldsWidgetGroupsPersistedComponentState,
pageLayoutId,
);
const ungroupedDraftState = useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const ungroupedPersistedState = useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsPersistedComponentState,
pageLayoutId,
);
const editorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const editorModePersistedState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModePersistedComponentState,
pageLayoutId,
);
const pageLayoutIsInitializedState = useAtomComponentStateCallbackState(
pageLayoutIsInitializedComponentState,
pageLayoutId,
);
const clearAllWidgetDraftStates = useCallback(() => {
store.set(hasInitializedState, {});
store.set(groupsDraftState, {});
store.set(groupsPersistedState, {});
store.set(ungroupedDraftState, {});
store.set(ungroupedPersistedState, {});
store.set(editorModeDraftState, {});
store.set(editorModePersistedState, {});
}, [
store,
hasInitializedState,
groupsDraftState,
groupsPersistedState,
ungroupedDraftState,
ungroupedPersistedState,
editorModeDraftState,
editorModePersistedState,
]);
const resetPageLayoutTabToDefault = useCallback(
async (tabId: string) => {
try {
await resetMutation({
variables: { id: tabId },
});
exitLayoutCustomizationMode();
clearAllWidgetDraftStates();
setIsPageLayoutInEditMode(false);
store.set(pageLayoutIsInitializedState, false);
invalidateMetadataStore();
} catch (error) {
if (CombinedGraphQLErrors.is(error)) {
handleMetadataError(error, {
primaryMetadataName: 'pageLayoutTab',
operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
}
},
[
resetMutation,
exitLayoutCustomizationMode,
clearAllWidgetDraftStates,
setIsPageLayoutInEditMode,
invalidateMetadataStore,
handleMetadataError,
enqueueErrorSnackBar,
],
);
return { resetPageLayoutTabToDefault };
};
@@ -1,5 +1,7 @@
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
@@ -8,12 +10,13 @@ import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useMutation } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -32,6 +35,10 @@ export const useResetPageLayoutWidgetToDefault = (
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { invalidateMetadataStore } = useInvalidateMetadataStore();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { closeSidePanelMenu } = useSidePanelMenu();
const { invalidateMetadataStore } = useInvalidateMetadataStore();
@@ -72,6 +79,11 @@ export const useResetPageLayoutWidgetToDefault = (
pageLayoutId,
);
const pageLayoutIsInitializedState = useAtomComponentStateCallbackState(
pageLayoutIsInitializedComponentState,
pageLayoutId,
);
const clearWidgetDraftState = useCallback(
(widgetId: string) => {
const removeWidgetEntry = <T>(prev: Record<string, T>) => {
@@ -107,8 +119,11 @@ export const useResetPageLayoutWidgetToDefault = (
variables: { id: widgetId },
});
closeSidePanelMenu();
exitLayoutCustomizationMode();
clearWidgetDraftState(widgetId);
setIsPageLayoutInEditMode(false);
store.set(pageLayoutIsInitializedState, false);
closeSidePanelMenu();
invalidateMetadataStore();
} catch (error) {
if (CombinedGraphQLErrors.is(error)) {
@@ -123,6 +138,9 @@ export const useResetPageLayoutWidgetToDefault = (
},
[
resetMutation,
exitLayoutCustomizationMode,
clearWidgetDraftState,
setIsPageLayoutInEditMode,
closeSidePanelMenu,
clearWidgetDraftState,
invalidateMetadataStore,
@@ -41,12 +41,14 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector<
}
}
return flatPageLayouts.map((flatPageLayout) => ({
const result = flatPageLayouts.map((flatPageLayout) => ({
...flatPageLayout,
tabs: (tabsByPageLayoutId.get(flatPageLayout.id) ?? []).map((tab) => ({
...tab,
widgets: widgetsByTabId.get(tab.id) ?? [],
})),
}));
return result;
},
});
@@ -13,14 +13,9 @@ const buildWidgetPosition = (
) => {
switch (tabLayoutMode) {
case PageLayoutTabLayoutMode.VERTICAL_LIST: {
const index =
widget.position?.__typename === 'PageLayoutWidgetVerticalListPosition'
? widget.position.index
: widgetIndex;
return {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index,
index: widgetIndex, // we should read in widget.position.index instead. Not sure why this change
};
}
case PageLayoutTabLayoutMode.CANVAS:
@@ -57,6 +52,7 @@ export const convertPageLayoutDraftToUpdateInput = (
id: tab.id,
title: tab.title,
position: tab.position,
icon: tab.icon ?? null,
layoutMode: tab.layoutMode,
widgets: widgets.map((widget, widgetIndex) => ({
id: widget.id,
@@ -133,6 +133,13 @@ export const useFieldsWidgetEditorGroupsData = ({
};
});
const viewFieldByFieldMetadataId = new Map(
(view.viewFields ?? []).map((viewField) => [
viewField.fieldMetadataId,
viewField,
]),
);
const lastGroup = groups[groups.length - 1];
const lastFieldPosition =
lastGroup.fields.length > 0
@@ -145,8 +152,23 @@ export const useFieldsWidgetEditorGroupsData = ({
startPosition: lastFieldPosition,
});
if (missingFields.length > 0) {
lastGroup.fields = [...lastGroup.fields, ...missingFields];
const missingFieldsWithViewFieldId = missingFields.map((field) => {
const existingViewField = viewFieldByFieldMetadataId.get(
field.fieldMetadataItem.id,
);
if (isDefined(existingViewField)) {
return { ...field, viewFieldId: existingViewField.id };
}
return field;
});
if (missingFieldsWithViewFieldId.length > 0) {
lastGroup.fields = [
...lastGroup.fields,
...missingFieldsWithViewFieldId,
];
}
return { groups, ungroupedFields: [], editorMode: 'grouped' };
@@ -165,6 +165,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
Icon={IconRefreshDot}
label={t`Reset to default`}
onClick={handleResetToDefault}
disabled={!widgetInEditMode.isOverridden}
/>
</SelectableListItem>
<SelectableListItem itemId="delete" onEnter={handleDelete}>
@@ -2,6 +2,7 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab';
import { useResetPageLayoutTabToDefault } from '@/page-layout/hooks/useResetPageLayoutTabToDefault';
import { useSetAsPinnedTab } from '@/page-layout/hooks/useSetAsPinnedTab';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
@@ -11,6 +12,8 @@ import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -21,10 +24,13 @@ import {
IconChevronRight,
IconCopyPlus,
IconPinned,
IconRefreshDot,
IconTrash,
} from 'twenty-ui/display';
import { PageLayoutType } from '~/generated-metadata/graphql';
const RESET_TAB_TO_DEFAULT_MODAL_ID = 'reset-tab-to-default-modal';
type SidePanelPageLayoutTabSettingsContentProps = {
pageLayoutId: string;
recordId: string;
@@ -63,6 +69,9 @@ export const SidePanelPageLayoutTabSettingsContent = ({
tabListInstanceId,
});
const { setAsPinnedTab } = useSetAsPinnedTab(pageLayoutId);
const { resetPageLayoutTabToDefault } =
useResetPageLayoutTabToDefault(pageLayoutId);
const { openModal } = useModal();
if (!isDefined(pageLayoutTabSettingsOpenTabId)) {
return null;
@@ -83,6 +92,14 @@ export const SidePanelPageLayoutTabSettingsContent = ({
const canSetAsPinned =
isRecordPage && !isAlreadyPinned && tabsSorted.length > 1;
const handleResetToDefault = () => {
openModal(RESET_TAB_TO_DEFAULT_MODAL_ID);
};
const handleConfirmReset = () => {
resetPageLayoutTabToDefault(tab.id);
};
const handleDelete = () => {
deleteTab(tab.id);
setPageLayoutTabSettingsOpenTabId(null);
@@ -94,6 +111,7 @@ export const SidePanelPageLayoutTabSettingsContent = ({
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
@@ -151,6 +169,18 @@ export const SidePanelPageLayoutTabSettingsContent = ({
onClick={() => duplicateTab(tab.id)}
/>
</SelectableListItem>
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
onEnter={handleResetToDefault}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
Icon={IconRefreshDot}
label={t`Reset to default`}
onClick={handleResetToDefault}
disabled={!tab.isOverridden}
/>
</SelectableListItem>
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
@@ -166,6 +196,14 @@ export const SidePanelPageLayoutTabSettingsContent = ({
)}
</SidePanelGroup>
</SidePanelList>
<ConfirmationModal
modalInstanceId={RESET_TAB_TO_DEFAULT_MODAL_ID}
title={t`Reset to default`}
subtitle={t`This will cancel all modifications done on the tab and its widgets. This action cannot be undone.`}
onConfirmClick={handleConfirmReset}
confirmButtonText={t`Reset`}
confirmButtonAccent="blue"
/>
</>
);
};
@@ -3,5 +3,6 @@ export const TAB_SETTINGS_SELECTABLE_ITEM_IDS = {
MOVE_RIGHT: 'tab-move-right',
SET_AS_PINNED: 'tab-set-as-pinned',
DUPLICATE: 'tab-duplicate',
RESET_TO_DEFAULT: 'tab-reset-to-default',
DELETE: 'tab-delete',
} as const;
@@ -0,0 +1,10 @@
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
export type CascadingIsOverriddenContext = {
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
workspaceCustomApplicationUniversalIdentifier: string;
};
@@ -15,7 +15,6 @@ import {
} from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -73,7 +72,7 @@ export class PageLayoutTabResolver {
@ResolveField(() => Boolean)
isOverridden(@Parent() tab: PageLayoutTabDTO): boolean {
return isDefined(tab.overrides) && Object.keys(tab.overrides).length > 0;
return tab.isOverridden;
}
@Query(() => [PageLayoutTabDTO])
@@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
@@ -15,6 +16,8 @@ import {
} from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-update-page-layout-tab-input-to-flat-page-layout-tab-to-update-or-throw.util';
import { reconstructFlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/create-page-layout-tab.input';
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab.input';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
@@ -46,8 +49,19 @@ export class PageLayoutTabService {
workspaceId: string;
pageLayoutId: string;
}): Promise<PageLayoutTabDTO[]> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
const {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getPageLayoutTabFlatEntityMaps(workspaceId);
const cascadingContext = await this.buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
});
return Object.values(flatPageLayoutTabMaps.byUniversalIdentifier)
.filter(isDefined)
@@ -61,6 +75,7 @@ export class PageLayoutTabService {
tab,
flatPageLayoutWidgetMaps,
}),
cascadingContext,
),
);
}
@@ -72,8 +87,19 @@ export class PageLayoutTabService {
id: string;
workspaceId: string;
}): Promise<PageLayoutTabDTO> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
const {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getPageLayoutTabFlatEntityMaps(workspaceId);
const cascadingContext = await this.buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
});
const flatTab = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
@@ -95,21 +121,54 @@ export class PageLayoutTabService {
tab: flatTab,
flatPageLayoutWidgetMaps,
}),
cascadingContext,
);
}
private async getPageLayoutTabFlatEntityMaps(workspaceId: string): Promise<{
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
}> {
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps', 'flatPageLayoutWidgetMaps'],
flatMapsKeys: [
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewFieldGroupMaps',
'flatViewFieldMaps',
],
},
);
}
private async buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
}: {
workspaceId: string;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
}): Promise<CascadingIsOverriddenContext> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
return {
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
};
}
async create({
createPageLayoutTabInput,
workspaceId,
@@ -0,0 +1,52 @@
import { isDefined } from 'twenty-shared/utils';
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';
const hasNonEmptyOverrides = (entity: { overrides: unknown }): boolean =>
isDefined(entity.overrides) &&
typeof entity.overrides === 'object' &&
entity.overrides !== null &&
Object.keys(entity.overrides).length > 0;
export const computeTabCascadingIsOverridden = ({
tab,
childWidgets,
widgetCascadingIsOverriddenById,
workspaceCustomApplicationUniversalIdentifier,
}: {
tab: FlatPageLayoutTab;
childWidgets: FlatPageLayoutWidget[];
widgetCascadingIsOverriddenById: Record<string, boolean>;
workspaceCustomApplicationUniversalIdentifier: string;
}): boolean => {
if (
tab.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
) {
return false;
}
if (hasNonEmptyOverrides(tab)) {
return true;
}
for (const widget of childWidgets) {
if (
widget.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
) {
return true;
}
if (!widget.isActive || hasNonEmptyOverrides(widget)) {
return true;
}
if (widgetCascadingIsOverriddenById[widget.id] === true) {
return true;
}
}
return false;
};
@@ -5,19 +5,26 @@ import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-t
export const fromFlatPageLayoutTabToPageLayoutTabDto = (
flatPageLayoutTab: FlatPageLayoutTab,
isOverriddenOverride?: boolean,
): Omit<PageLayoutTabDTO, 'widgets'> => {
const {
createdAt,
updatedAt,
deletedAt,
widgetIds: _widgetIds,
overrides,
...rest
} = flatPageLayoutTab;
const resolvedOverrides = isDefined(overrides) ? overrides : {};
return {
...rest,
...resolvedOverrides,
overrides,
isOverridden:
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
isOverriddenOverride ??
(isDefined(overrides) && Object.keys(overrides).length > 0),
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,
@@ -1,15 +1,58 @@
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
import { type FlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import { computeTabCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-tab/utils/compute-tab-cascading-is-overridden.util';
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
import { sortWidgetDtosByPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/sort-widget-dtos-by-position.util';
import { computeWidgetCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-widget/utils/compute-widget-cascading-is-overridden.util';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
export const fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto = (
flatPageLayoutTabWithWidgets: FlatPageLayoutTabWithWidgets,
cascadingContext?: CascadingIsOverriddenContext,
): PageLayoutTabDTO => {
const { widgets, ...flatPageLayoutTab } = flatPageLayoutTabWithWidgets;
if (!cascadingContext) {
const widgetDtos = widgets.map((widget) =>
fromFlatPageLayoutWidgetToPageLayoutWidgetDto(widget),
);
return {
...fromFlatPageLayoutTabToPageLayoutTabDto(flatPageLayoutTab),
widgets: sortWidgetDtosByPosition(widgetDtos),
};
}
const widgetCascadingIsOverriddenById: Record<string, boolean> = {};
const widgetDtos = widgets.map((widget) => {
const isOverridden = computeWidgetCascadingIsOverridden({
widget,
flatViewFieldGroupMaps: cascadingContext.flatViewFieldGroupMaps,
flatViewFieldMaps: cascadingContext.flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
cascadingContext.workspaceCustomApplicationUniversalIdentifier,
});
widgetCascadingIsOverriddenById[widget.id] = isOverridden;
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(widget, isOverridden);
});
const tabIsOverridden = computeTabCascadingIsOverridden({
tab: flatPageLayoutTab,
childWidgets: widgets,
widgetCascadingIsOverriddenById,
workspaceCustomApplicationUniversalIdentifier:
cascadingContext.workspaceCustomApplicationUniversalIdentifier,
});
return {
...fromFlatPageLayoutTabToPageLayoutTabDto(flatPageLayoutTab),
widgets: widgets.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto),
...fromFlatPageLayoutTabToPageLayoutTabDto(
flatPageLayoutTab,
tabIsOverridden,
),
widgets: sortWidgetDtosByPosition(widgetDtos),
};
};
@@ -6,9 +6,7 @@ import {
} from '@nestjs/common';
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -23,7 +21,6 @@ import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-wid
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@MetadataResolver(() => PageLayoutWidgetDTO)
@@ -98,21 +95,6 @@ export class PageLayoutWidgetResolver {
});
}
@ResolveField(() => String)
title(@Parent() widget: PageLayoutWidgetDTO): string {
return resolveOverridableEntityProperty(widget, 'title');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
position(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'position');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
conditionalDisplay(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'conditionalDisplay');
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
configuration(@Parent() widget: PageLayoutWidgetDTO) {
return widget.configuration;
@@ -120,8 +102,6 @@ export class PageLayoutWidgetResolver {
@ResolveField(() => Boolean)
isOverridden(@Parent() widget: PageLayoutWidgetDTO): boolean {
return (
isDefined(widget.overrides) && Object.keys(widget.overrides).length > 0
);
return widget.isOverridden;
}
}
@@ -8,6 +8,8 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util';
import { fromDestroyPageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-destroy-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
@@ -26,6 +28,7 @@ import {
generatePageLayoutWidgetExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
import { computeWidgetCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-widget/utils/compute-widget-cascading-is-overridden.util';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
import { validateChartConfigurationFieldReferencesOrThrow } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-chart-configuration-field-references.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
@@ -61,6 +64,25 @@ export class PageLayoutWidgetService {
return flatPageLayoutWidgetMaps;
}
private async getWidgetAndViewFieldFlatEntityMaps(
workspaceId: string,
): Promise<{
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
}> {
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPageLayoutWidgetMaps',
'flatViewFieldGroupMaps',
'flatViewFieldMaps',
],
},
);
}
private async validateAndRunWidgetMigration({
workspaceId,
operations,
@@ -155,8 +177,16 @@ export class PageLayoutWidgetService {
workspaceId: string;
pageLayoutTabId: string;
}): Promise<PageLayoutWidgetDTO[]> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const {
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getWidgetAndViewFieldFlatEntityMaps(workspaceId);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
return Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier)
.filter(isDefined)
@@ -170,7 +200,18 @@ export class PageLayoutWidgetService {
new Date(widgetA.createdAt).getTime() -
new Date(widgetB.createdAt).getTime(),
)
.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto);
.map((widget) =>
fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
widget,
computeWidgetCascadingIsOverridden({
widget,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
}),
),
);
}
async findByIdOrThrow({
@@ -180,8 +221,16 @@ export class PageLayoutWidgetService {
id: string;
workspaceId: string;
}): Promise<PageLayoutWidgetDTO> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const {
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getWidgetAndViewFieldFlatEntityMaps(workspaceId);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatWidget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
@@ -198,7 +247,16 @@ export class PageLayoutWidgetService {
);
}
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(flatWidget);
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
flatWidget,
computeWidgetCascadingIsOverridden({
widget: flatWidget,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
}),
);
}
async create({
@@ -0,0 +1,95 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
const hasNonEmptyOverrides = (entity: { overrides: unknown }): boolean =>
isDefined(entity.overrides) &&
typeof entity.overrides === 'object' &&
entity.overrides !== null &&
Object.keys(entity.overrides).length > 0;
export const computeWidgetCascadingIsOverridden = ({
widget,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier,
}: {
widget: FlatPageLayoutWidget;
flatViewFieldGroupMaps: {
byUniversalIdentifier: Partial<
Record<string, FlatViewFieldGroup | undefined>
>;
};
flatViewFieldMaps: {
byUniversalIdentifier: Partial<Record<string, FlatViewField | undefined>>;
};
workspaceCustomApplicationUniversalIdentifier: string;
}): boolean => {
if (
widget.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
) {
return false;
}
if (hasNonEmptyOverrides(widget)) {
return true;
}
if (
!isFlatPageLayoutWidgetConfigurationOfType(
widget,
WidgetConfigurationType.FIELDS,
)
) {
return false;
}
const viewId = widget.configuration.viewId;
if (!isDefined(viewId)) {
return false;
}
const viewFieldGroups = Object.values(
flatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((group) => group.viewId === viewId && !isDefined(group.deletedAt));
for (const group of viewFieldGroups) {
if (
group.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
) {
return true;
}
if (!group.isActive || hasNonEmptyOverrides(group)) {
return true;
}
}
const viewFields = Object.values(flatViewFieldMaps.byUniversalIdentifier)
.filter(isDefined)
.filter((field) => field.viewId === viewId && !isDefined(field.deletedAt));
for (const field of viewFields) {
if (
field.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
) {
return true;
}
if (!field.isActive || hasNonEmptyOverrides(field)) {
return true;
}
}
return false;
};
@@ -5,14 +5,26 @@ import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layou
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
flatPageLayoutWidget: FlatPageLayoutWidget,
isOverriddenOverride?: boolean,
): PageLayoutWidgetDTO => {
const { createdAt, updatedAt, deletedAt, objectMetadataId, ...rest } =
flatPageLayoutWidget;
const {
createdAt,
updatedAt,
deletedAt,
objectMetadataId,
overrides,
...rest
} = flatPageLayoutWidget;
const resolvedOverrides = isDefined(overrides) ? overrides : {};
return {
...rest,
...resolvedOverrides,
overrides,
isOverridden:
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
isOverriddenOverride ??
(isDefined(overrides) && Object.keys(overrides).length > 0),
objectMetadataId: objectMetadataId ?? undefined,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
@@ -0,0 +1,26 @@
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const sortWidgetDtosByPosition = (
widgets: PageLayoutWidgetDTO[],
): PageLayoutWidgetDTO[] => {
return [...widgets].sort((a, b) => {
const posA = a.position;
const posB = b.position;
if (!posA || !posB) return 0;
if ('index' in posA && 'index' in posB) {
return posA.index - posB.index;
}
if ('row' in posA && 'row' in posB) {
if (posA.row !== posB.row) return posA.row - posB.row;
if ('column' in posA && 'column' in posB) {
return posA.column - posB.column;
}
}
return 0;
});
};
@@ -8,13 +8,14 @@ import { Args, Mutation, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
@@ -136,4 +137,16 @@ export class PageLayoutResolver {
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutTabDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async resetPageLayoutTabToDefault(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
return this.pageLayoutResetService.resetPageLayoutTabToDefault({
id,
workspaceId: workspace.id,
});
}
}
@@ -4,13 +4,23 @@ import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { splitEntitiesByResetStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util';
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 { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import {
PageLayoutTabException,
PageLayoutTabExceptionCode,
PageLayoutTabExceptionMessageKey,
generatePageLayoutTabExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import {
PageLayoutWidgetException,
@@ -18,8 +28,8 @@ import {
PageLayoutWidgetExceptionMessageKey,
generatePageLayoutWidgetExceptionMessage,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
@@ -33,6 +43,7 @@ export class PageLayoutResetService {
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
private readonly dashboardSyncService: DashboardSyncService,
private readonly viewService: ViewService,
) {}
async resetPageLayoutWidgetToDefault({
@@ -187,6 +198,309 @@ export class PageLayoutResetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(updatedWidget);
}
async resetPageLayoutTabToDefault({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewFieldGroupMaps',
'flatViewFieldMaps',
],
},
);
const tab = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPageLayoutTabMaps,
});
if (!isDefined(tab) || isDefined(tab.deletedAt)) {
throw new PageLayoutTabException(
generatePageLayoutTabExceptionMessage(
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
id,
),
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
);
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
if (
tab.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
) {
throw new PageLayoutTabException(
`Custom tab "${id}" cannot be reset to default`,
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
);
}
const now = new Date().toISOString();
const tabToUpdate: FlatPageLayoutTab = {
...tab,
overrides: null,
updatedAt: now,
};
const {
widgetsToUpdate,
widgetsToDelete,
viewFieldGroupsToUpdate,
viewFieldGroupsToDelete,
viewFieldsToUpdate,
viewFieldsToDelete,
orphanedViewIds,
} = this.computeTabChildResetOperations({
tabId: id,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
now,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToUpdate: [tabToUpdate],
flatEntityToDelete: [],
},
pageLayoutWidget: {
flatEntityToCreate: [],
flatEntityToUpdate: widgetsToUpdate,
flatEntityToDelete: widgetsToDelete,
},
viewFieldGroup: {
flatEntityToCreate: [],
flatEntityToUpdate: viewFieldGroupsToUpdate,
flatEntityToDelete: viewFieldGroupsToDelete,
},
viewField: {
flatEntityToCreate: [],
flatEntityToUpdate: viewFieldsToUpdate,
flatEntityToDelete: viewFieldsToDelete,
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while resetting page layout tab to default',
);
}
await this.destroyOrphanedFieldsWidgetViews({
viewIds: orphanedViewIds,
workspaceId,
});
const { flatPageLayoutTabMaps: recomputedTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const updatedTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(updatedTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(updatedTab);
}
private computeTabChildResetOperations({
tabId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
}: {
tabId: string;
flatPageLayoutWidgetMaps: {
byUniversalIdentifier: Record<string, FlatPageLayoutWidget | undefined>;
};
flatViewFieldGroupMaps: {
byUniversalIdentifier: Record<string, FlatViewFieldGroup | undefined>;
};
flatViewFieldMaps: {
byUniversalIdentifier: Record<string, FlatViewField | undefined>;
};
workspaceCustomApplicationUniversalIdentifier: string;
now: string;
}): {
widgetsToUpdate: FlatPageLayoutWidget[];
widgetsToDelete: FlatPageLayoutWidget[];
viewFieldGroupsToUpdate: FlatViewFieldGroup[];
viewFieldGroupsToDelete: FlatViewFieldGroup[];
viewFieldsToUpdate: FlatViewField[];
viewFieldsToDelete: FlatViewField[];
orphanedViewIds: string[];
} {
const existingWidgets = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
widget.pageLayoutTabId === tabId && !isDefined(widget.deletedAt),
);
const { toHardDelete: widgetsToDelete, toReset: widgetsToReset } =
splitEntitiesByResetStrategy({
entities: existingWidgets,
workspaceCustomApplicationUniversalIdentifier,
now,
});
let allViewFieldGroupsToUpdate: FlatViewFieldGroup[] = [];
let allViewFieldGroupsToDelete: FlatViewFieldGroup[] = [];
let allViewFieldsToUpdate: FlatViewField[] = [];
let allViewFieldsToDelete: FlatViewField[] = [];
for (const widget of widgetsToReset) {
if (
!isFlatPageLayoutWidgetConfigurationOfType(
widget,
WidgetConfigurationType.FIELDS,
)
) {
continue;
}
const viewId = widget.configuration.viewId;
if (!isDefined(viewId)) {
continue;
}
const {
viewFieldGroupsToUpdate,
viewFieldGroupsToDelete,
viewFieldsToUpdate,
viewFieldsToDelete,
} = this.computeFieldsWidgetChildResetOperations({
viewId,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
});
allViewFieldGroupsToUpdate = [
...allViewFieldGroupsToUpdate,
...viewFieldGroupsToUpdate,
];
allViewFieldGroupsToDelete = [
...allViewFieldGroupsToDelete,
...viewFieldGroupsToDelete,
];
allViewFieldsToUpdate = [...allViewFieldsToUpdate, ...viewFieldsToUpdate];
allViewFieldsToDelete = [...allViewFieldsToDelete, ...viewFieldsToDelete];
}
const orphanedViewIds =
this.collectOrphanedViewIdsFromDeletedWidgets(widgetsToDelete);
return {
widgetsToUpdate: widgetsToReset,
widgetsToDelete,
viewFieldGroupsToUpdate: allViewFieldGroupsToUpdate,
viewFieldGroupsToDelete: allViewFieldGroupsToDelete,
viewFieldsToUpdate: allViewFieldsToUpdate,
viewFieldsToDelete: allViewFieldsToDelete,
orphanedViewIds,
};
}
private collectOrphanedViewIdsFromDeletedWidgets(
widgets: FlatPageLayoutWidget[],
): string[] {
const viewIds: string[] = [];
for (const widget of widgets) {
const viewId = this.getViewIdFromFieldsWidget(widget);
if (isDefined(viewId)) {
viewIds.push(viewId);
}
}
return viewIds;
}
private getViewIdFromFieldsWidget(
widget: FlatPageLayoutWidget,
): string | undefined {
if (
widget.configuration.configurationType !== WidgetConfigurationType.FIELDS
) {
return undefined;
}
const viewId = (widget.configuration as { viewId?: string | null }).viewId;
return typeof viewId === 'string' ? viewId : undefined;
}
private async destroyOrphanedFieldsWidgetViews({
viewIds,
workspaceId,
}: {
viewIds: string[];
workspaceId: string;
}): Promise<void> {
for (const viewId of viewIds) {
try {
await this.viewService.destroyOne({
destroyViewInput: { id: viewId },
workspaceId,
});
} catch (error) {
this.logger.warn(
`Failed to destroy orphaned view ${viewId} during tab reset: ${error}`,
);
}
}
}
private computeFieldsWidgetChildResetOperations({
viewId,
flatViewFieldGroupMaps,
@@ -10,13 +10,13 @@ import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util';
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { buildFlatPageLayoutWidgetCommonProperties } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/build-flat-page-layout-widget-common-properties.util';
import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
@@ -33,9 +33,12 @@ import {
generatePageLayoutExceptionMessage,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
type UpdatePageLayoutWithTabsParams = {
@@ -322,39 +325,61 @@ export class PageLayoutUpdateService {
},
);
const tabsToUpdate: FlatPageLayoutTab[] = entitiesToUpdate.map(
(tabInput) => {
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: tabInput.id,
flatEntityMaps: flatPageLayoutTabMaps,
const buildTabUpdateWithOverrides = (
tabInput: UpdatePageLayoutTabWithWidgetsInput,
additionalProps?: Partial<FlatPageLayoutTab>,
): FlatPageLayoutTab => {
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: tabInput.id,
flatEntityMaps: flatPageLayoutTabMaps,
});
const editableProperties: Record<string, unknown> = {
title: tabInput.title,
position: tabInput.position,
...(tabInput.icon !== undefined ? { icon: tabInput.icon } : {}),
...(tabInput.layoutMode !== undefined
? { layoutMode: tabInput.layoutMode }
: {}),
};
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingTab.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutTab',
existingFlatEntity: existingTab,
updatedEditableProperties: editableProperties,
shouldOverride,
});
return {
...existingTab,
title: tabInput.title,
position: tabInput.position,
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
updatedAt: now.toISOString(),
};
},
);
return {
...mergeUpdateInExistingRecord({
existing: existingTab,
properties: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
update: updatedEditableProperties,
}),
overrides,
updatedAt: now.toISOString(),
...additionalProps,
} as FlatPageLayoutTab;
};
const tabsToUpdate: FlatPageLayoutTab[] =
entitiesToUpdate.map((tabInput) =>
buildTabUpdateWithOverrides(tabInput),
);
const tabsToRestoreAndUpdate: FlatPageLayoutTab[] =
entitiesToRestoreAndUpdate.map((tabInput) => {
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: tabInput.id,
flatEntityMaps: flatPageLayoutTabMaps,
});
return {
...existingTab,
title: tabInput.title,
position: tabInput.position,
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
isActive: true,
updatedAt: now.toISOString(),
};
});
entitiesToRestoreAndUpdate.map((tabInput) =>
buildTabUpdateWithOverrides(tabInput, { isActive: true }),
);
const tabsToRemove = idsToRemove
.map((tabId) =>
@@ -548,77 +573,103 @@ export class PageLayoutUpdateService {
},
);
const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map(
(widgetInput) => {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
const buildWidgetUpdateWithOverrides = (
widgetInput: UpdatePageLayoutWidgetWithIdInput,
additionalProps?: Partial<FlatPageLayoutWidget>,
): FlatPageLayoutWidget => {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const editableProperties: Record<string, unknown> = {
title: widgetInput.title,
type: widgetInput.type,
objectMetadataId: widgetInput.objectMetadataId,
gridPosition: widgetInput.gridPosition,
position: widgetInput.position,
...(widgetInput.configuration !== undefined
? { configuration: widgetInput.configuration }
: {}),
...(widgetInput.conditionalDisplay !== undefined
? { conditionalDisplay: widgetInput.conditionalDisplay }
: {}),
};
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: editableProperties,
shouldOverride,
});
const updatedConfiguration = widgetInput.configuration ?? null;
const {
pageLayoutTabUniversalIdentifier,
objectMetadataUniversalIdentifier,
} = resolveEntityRelationUniversalIdentifiers({
metadataName: 'pageLayoutWidget',
foreignKeyValues: {
pageLayoutTabId: widgetInput.pageLayoutTabId,
objectMetadataId: widgetInput.objectMetadataId,
},
flatEntityMaps: { flatPageLayoutTabMaps, flatObjectMetadataMaps },
});
return {
...existingWidget,
...buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
}),
configuration: updatedConfiguration,
updatedAt: now.toISOString(),
...(isDefined(updatedConfiguration) && {
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: updatedConfiguration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById:
flatViewMaps.universalIdentifierById,
}),
}),
};
},
);
const mergedWidget = {
...mergeUpdateInExistingRecord({
existing: existingWidget,
properties: [
...FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
],
update: updatedEditableProperties,
}),
overrides,
pageLayoutTabId: widgetInput.pageLayoutTabId,
pageLayoutTabUniversalIdentifier,
objectMetadataUniversalIdentifier,
updatedAt: now.toISOString(),
...additionalProps,
} as FlatPageLayoutWidget;
const resolvedConfiguration =
mergedWidget.configuration ?? existingWidget.configuration;
if (isDefined(resolvedConfiguration)) {
mergedWidget.universalConfiguration =
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: resolvedConfiguration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById:
flatViewMaps.universalIdentifierById,
});
}
return mergedWidget;
};
const widgetsToUpdate: FlatPageLayoutWidget[] =
entitiesToUpdate.map((widgetInput) =>
buildWidgetUpdateWithOverrides(widgetInput),
);
const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] =
entitiesToRestoreAndUpdate.map((widgetInput) => {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const restoredConfiguration = widgetInput.configuration ?? null;
return {
...existingWidget,
...buildFlatPageLayoutWidgetCommonProperties({
widgetInput,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
}),
configuration: restoredConfiguration,
isActive: true,
updatedAt: now.toISOString(),
...(isDefined(restoredConfiguration) && {
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: restoredConfiguration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById:
flatViewMaps.universalIdentifierById,
}),
}),
};
});
entitiesToRestoreAndUpdate.map((widgetInput) =>
buildWidgetUpdateWithOverrides(widgetInput, { isActive: true }),
);
const widgetsToRemove = idsToRemove
.map((widgetId) =>
@@ -5,11 +5,14 @@ import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { fromCreatePageLayoutInputToFlatPageLayoutToCreate } from 'src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util';
import { fromDestroyPageLayoutInputToFlatPageLayoutOrThrow } from 'src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util';
import {
@@ -50,8 +53,17 @@ export class PageLayoutService {
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
const cascadingContext = await this.buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
});
const activeLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)
@@ -65,6 +77,7 @@ export class PageLayoutService {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
}),
cascadingContext,
),
);
}
@@ -83,8 +96,17 @@ export class PageLayoutService {
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
const cascadingContext = await this.buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
});
const activeLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)
@@ -108,6 +130,7 @@ export class PageLayoutService {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
}),
cascadingContext,
),
);
}
@@ -123,8 +146,17 @@ export class PageLayoutService {
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
const cascadingContext = await this.buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
});
const flatLayout = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPageLayoutMaps,
@@ -149,6 +181,7 @@ export class PageLayoutService {
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
}),
cascadingContext,
);
}
@@ -156,6 +189,8 @@ export class PageLayoutService {
flatPageLayoutMaps: FlatPageLayoutMaps;
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
}> {
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -164,11 +199,38 @@ export class PageLayoutService {
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewFieldGroupMaps',
'flatViewFieldMaps',
],
},
);
}
private async buildCascadingIsOverriddenContext({
workspaceId,
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
}: {
workspaceId: string;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
flatViewFieldMaps: FlatViewFieldMaps;
}): Promise<CascadingIsOverriddenContext> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
return {
flatPageLayoutWidgetMaps,
flatViewFieldGroupMaps,
flatViewFieldMaps,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
};
}
async create({
createPageLayoutInput,
workspaceId,
@@ -1,3 +1,4 @@
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
import { type FlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
import { fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util';
@@ -5,11 +6,14 @@ import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/p
export const fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto = (
flatPageLayoutWithTabsAndWidgets: FlatPageLayoutWithTabsAndWidgets,
cascadingContext?: CascadingIsOverriddenContext,
): PageLayoutDTO => {
const { tabs, ...flatPageLayout } = flatPageLayoutWithTabsAndWidgets;
return {
...fromFlatPageLayoutToPageLayoutDto(flatPageLayout),
tabs: tabs.map(fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto),
tabs: tabs.map((tab) =>
fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto(tab, cascadingContext),
),
};
};
@@ -1,4 +1,5 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
@@ -21,6 +22,18 @@ export const flatEntityToScalarFlatEntity = <T extends AllMetadataName>({
result[key] = flatEntityRecord[key];
}
const overrides = flatEntityRecord.overrides;
if (isDefined(overrides) && typeof overrides === 'object') {
for (const [key, value] of Object.entries(
overrides as Record<string, unknown>,
)) {
if (key in config) {
result[key] = value;
}
}
}
result.id = flatEntityRecord.id;
result.workspaceId = flatEntityRecord.workspaceId;
result.applicationId = flatEntityRecord.applicationId;
@@ -0,0 +1,48 @@
import { LazyEmbed } from '@/design-system/components';
import type { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const StyledIllustrationContainer = styled.div`
justify-self: stretch;
margin-top: ${theme.spacing(6)};
max-width: 100%;
min-width: 0;
width: 100%;
`;
const StyledIllustrationEmbed = styled(LazyEmbed)`
border: none;
display: block;
height: 462px;
max-width: 100%;
min-width: 0;
width: 100%;
`;
type IllustrationProps = {
illustration: IllustrationType;
backgroundColor: string;
};
export function Illustration({
illustration,
backgroundColor,
}: IllustrationProps) {
return (
<StyledIllustrationContainer>
<StyledIllustrationEmbed
eager
src={illustration.src}
title={illustration.title}
unloadWhenHidden={false}
allow="clipboard-write; encrypted-media; gyroscope; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
style={{
backgroundColor: backgroundColor,
}}
/>
</StyledIllustrationContainer>
);
}
@@ -0,0 +1,146 @@
'use client';
import { Center, useGLTF, Float, PresentationControls } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';
import { Suspense, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import * as THREE from 'three';
function Model({ url, color }: { url: string; color: string }) {
const { scene } = useGLTF(url);
const groupRef = useRef<THREE.Group>(null);
const [hovered, setHovered] = useState(false);
const material = useMemo(
() =>
new THREE.MeshStandardMaterial({
color: new THREE.Color(color),
emissive: new THREE.Color(color),
emissiveIntensity: 0.15,
roughness: 0.2,
metalness: 0.8,
wireframe: true,
transparent: true,
opacity: 0.8,
}),
[color],
);
const coloredScene = useMemo(() => {
const clonedScene = scene.clone();
clonedScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = material;
}
});
return clonedScene;
}, [scene, material]);
useEffect(() => {
return () => {
material.dispose();
coloredScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry?.dispose();
}
});
};
}, [material, coloredScene]);
const handlePointerOver = useCallback(() => {
setHovered(true);
}, []);
const handlePointerOut = useCallback(() => {
setHovered(false);
}, []);
useFrame((state, delta) => {
if (!groupRef.current) return;
const amp = hovered ? 1.8 : 1.2;
const targetRotationX = (-state.pointer.y * Math.PI * amp) / 8;
const targetRotationY = (state.pointer.x * Math.PI * amp) / 8;
groupRef.current.rotation.x = THREE.MathUtils.damp(
groupRef.current.rotation.x,
targetRotationX,
4,
delta,
);
groupRef.current.rotation.y = THREE.MathUtils.damp(
groupRef.current.rotation.y,
targetRotationY,
4,
delta,
);
groupRef.current.rotation.z = THREE.MathUtils.damp(
groupRef.current.rotation.z,
Math.sin(state.clock.elapsedTime * 0.4) * 0.08,
2,
delta,
);
const targetScale = hovered ? 1.15 : 1.0;
groupRef.current.scale.setScalar(
THREE.MathUtils.damp(groupRef.current.scale.x, targetScale, 6, delta),
);
});
return (
<group
ref={groupRef}
onPointerOver={handlePointerOver}
onPointerOut={handlePointerOut}
>
<Float speed={2.5} rotationIntensity={0.4} floatIntensity={0.8}>
<PresentationControls
global
rotation={[0.13, 0.1, 0]}
polar={[-0.4, 0.2]}
azimuth={[-1, 0.75]}
snap={true}
>
<Center>
<primitive object={coloredScene} scale={1.2} />
</Center>
</PresentationControls>
</Float>
</group>
);
}
export function GlbViewer({ src }: { src: string }) {
return (
<Canvas
camera={{ position: [0, 0, 6], fov: 45 }}
style={{ width: '100%', height: '100%', cursor: 'pointer' }}
dpr={[1, 1.5]}
frameloop="demand"
gl={{
antialias: true,
alpha: true,
powerPreference: 'low-power',
}}
>
<ambientLight intensity={0.6} />
<directionalLight position={[10, 10, 5]} intensity={2.5} color="#ffffff" />
<directionalLight
position={[-10, -10, -5]}
intensity={1.5}
color="#120AF2"
/>
<spotLight
position={[0, 5, 10]}
intensity={2.5}
penumbra={0.8}
color="#ffffff"
angle={0.6}
/>
<Suspense fallback={null}>
<Model url={src} color="#120AF2" />
</Suspense>
</Canvas>
);
}