Reset Fields widget implementation (#19283)
## Context This PR implements the first steps for overridable entities resets. <img width="1277" height="568" alt="Screenshot 2026-04-02 at 18 58 04" src="https://github.com/user-attachments/assets/4c7f93b1-c453-4905-a919-cd6af11e0e16" />
This commit is contained in:
@@ -3459,6 +3459,7 @@ type Mutation {
|
||||
updatePageLayout(id: String!, input: UpdatePageLayoutInput!): PageLayout!
|
||||
destroyPageLayout(id: String!): Boolean!
|
||||
updatePageLayoutWithTabsAndWidgets(id: String!, input: UpdatePageLayoutWithTabsInput!): PageLayout!
|
||||
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
|
||||
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
destroyPageLayoutWidget(id: String!): Boolean!
|
||||
|
||||
@@ -2906,6 +2906,7 @@ export interface Mutation {
|
||||
updatePageLayout: PageLayout
|
||||
destroyPageLayout: Scalars['Boolean']
|
||||
updatePageLayoutWithTabsAndWidgets: PageLayout
|
||||
resetPageLayoutWidgetToDefault: PageLayoutWidget
|
||||
createPageLayoutWidget: PageLayoutWidget
|
||||
updatePageLayoutWidget: PageLayoutWidget
|
||||
destroyPageLayoutWidget: Scalars['Boolean']
|
||||
@@ -6155,6 +6156,7 @@ export interface MutationGenqlSelection{
|
||||
updatePageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutInput} })
|
||||
destroyPageLayout?: { __args: {id: Scalars['String']} }
|
||||
updatePageLayoutWithTabsAndWidgets?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWithTabsInput} })
|
||||
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
|
||||
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
|
||||
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
|
||||
|
||||
@@ -7782,6 +7782,15 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"resetPageLayoutWidgetToDefault": [
|
||||
75,
|
||||
{
|
||||
"id": [
|
||||
1,
|
||||
"String!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createPageLayoutWidget": [
|
||||
75,
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
+12
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { PAGE_LAYOUT_WIDGET_FRAGMENT } from '@/page-layout/graphql/fragments/pageLayoutWidgetFragment';
|
||||
|
||||
export const RESET_PAGE_LAYOUT_WIDGET_TO_DEFAULT = gql`
|
||||
${PAGE_LAYOUT_WIDGET_FRAGMENT}
|
||||
mutation ResetPageLayoutWidgetToDefault($id: String!) {
|
||||
resetPageLayoutWidgetToDefault(id: $id) {
|
||||
...PageLayoutWidgetFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
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 { 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 { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { ResetPageLayoutWidgetToDefaultDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useResetPageLayoutWidgetToDefault = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const [resetMutation] = useMutation(ResetPageLayoutWidgetToDefaultDocument);
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const { invalidateMetadataStore } = useInvalidateMetadataStore();
|
||||
|
||||
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 clearWidgetDraftState = useCallback(
|
||||
(widgetId: string) => {
|
||||
const removeWidgetEntry = <T>(prev: Record<string, T>) => {
|
||||
const { [widgetId]: _, ...rest } = prev;
|
||||
|
||||
return rest;
|
||||
};
|
||||
|
||||
store.set(hasInitializedState, removeWidgetEntry);
|
||||
store.set(groupsDraftState, removeWidgetEntry);
|
||||
store.set(groupsPersistedState, removeWidgetEntry);
|
||||
store.set(ungroupedDraftState, removeWidgetEntry);
|
||||
store.set(ungroupedPersistedState, removeWidgetEntry);
|
||||
store.set(editorModeDraftState, removeWidgetEntry);
|
||||
store.set(editorModePersistedState, removeWidgetEntry);
|
||||
},
|
||||
[
|
||||
store,
|
||||
hasInitializedState,
|
||||
groupsDraftState,
|
||||
groupsPersistedState,
|
||||
ungroupedDraftState,
|
||||
ungroupedPersistedState,
|
||||
editorModeDraftState,
|
||||
editorModePersistedState,
|
||||
],
|
||||
);
|
||||
|
||||
const resetPageLayoutWidgetToDefault = useCallback(
|
||||
async (widgetId: string) => {
|
||||
try {
|
||||
await resetMutation({
|
||||
variables: { id: widgetId },
|
||||
});
|
||||
|
||||
closeSidePanelMenu();
|
||||
clearWidgetDraftState(widgetId);
|
||||
invalidateMetadataStore();
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
handleMetadataError(error, {
|
||||
primaryMetadataName: 'pageLayoutWidget',
|
||||
operationType: CrudOperationType.UPDATE,
|
||||
});
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`An error occurred.` });
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
resetMutation,
|
||||
closeSidePanelMenu,
|
||||
clearWidgetDraftState,
|
||||
invalidateMetadataStore,
|
||||
handleMetadataError,
|
||||
enqueueErrorSnackBar,
|
||||
],
|
||||
);
|
||||
|
||||
return { resetPageLayoutWidgetToDefault };
|
||||
};
|
||||
+39
@@ -1,6 +1,7 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useResetPageLayoutWidgetToDefault } from '@/page-layout/hooks/useResetPageLayoutWidgetToDefault';
|
||||
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
@@ -11,6 +12,8 @@ import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
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 { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -18,10 +21,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconLayoutSidebarRight,
|
||||
IconRefreshDot,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
const RESET_WIDGET_TO_DEFAULT_MODAL_ID = 'reset-widget-to-default-modal';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -46,6 +52,11 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const { resetPageLayoutWidgetToDefault } =
|
||||
useResetPageLayoutWidgetToDefault(pageLayoutId);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const fieldsConfiguration = widgetInEditMode?.configuration as
|
||||
@@ -82,6 +93,14 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
openModal(RESET_WIDGET_TO_DEFAULT_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleConfirmReset = () => {
|
||||
resetPageLayoutWidgetToDefault(widgetInEditMode.id);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deletePageLayoutWidget(widgetInEditMode.id);
|
||||
};
|
||||
@@ -96,6 +115,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
'move-to-tab',
|
||||
'add-widget-above',
|
||||
'add-widget-below',
|
||||
'reset-to-default',
|
||||
'delete',
|
||||
];
|
||||
|
||||
@@ -136,6 +156,17 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
<SidePanelGroup heading={t`Manage`}>
|
||||
<SelectableListItem
|
||||
itemId="reset-to-default"
|
||||
onEnter={handleResetToDefault}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="reset-to-default"
|
||||
Icon={IconRefreshDot}
|
||||
label={t`Reset to default`}
|
||||
onClick={handleResetToDefault}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem itemId="delete" onEnter={handleDelete}>
|
||||
<CommandMenuItem
|
||||
id="delete"
|
||||
@@ -148,6 +179,14 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
</SidePanelList>
|
||||
</StyledSidePanelContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_WIDGET_TO_DEFAULT_MODAL_ID}
|
||||
title={t`Reset to default`}
|
||||
subtitle={t`This will cancel all modifications done on the widget. This action cannot be undone.`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
type EntityWithApplicationIdentifierAndOverrides = {
|
||||
applicationUniversalIdentifier: string;
|
||||
isActive: boolean;
|
||||
overrides: unknown;
|
||||
};
|
||||
|
||||
export const splitEntitiesByResetStrategy = <
|
||||
T extends EntityWithApplicationIdentifierAndOverrides,
|
||||
>({
|
||||
entities,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
entities: T[];
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): {
|
||||
toHardDelete: T[];
|
||||
toReset: (T & { isActive: true; overrides: null; updatedAt: string })[];
|
||||
} => {
|
||||
const toHardDelete: T[] = [];
|
||||
const toReset: (T & {
|
||||
isActive: true;
|
||||
overrides: null;
|
||||
updatedAt: string;
|
||||
})[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
entity.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
toHardDelete.push(entity);
|
||||
} else {
|
||||
toReset.push({
|
||||
...entity,
|
||||
isActive: true as const,
|
||||
overrides: null,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { toHardDelete, toReset };
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { PageLayoutController } from 'src/engine/metadata-modules/page-layout/co
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { PageLayoutResolver } from 'src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver';
|
||||
import { PageLayoutDuplicationService } from 'src/engine/metadata-modules/page-layout/services/page-layout-duplication.service';
|
||||
import { PageLayoutResetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-reset.service';
|
||||
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -45,6 +46,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
|
||||
PageLayoutService,
|
||||
PageLayoutDuplicationService,
|
||||
PageLayoutResolver,
|
||||
PageLayoutResetService,
|
||||
PageLayoutUpdateService,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
|
||||
+15
@@ -15,11 +15,13 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
|
||||
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 { 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';
|
||||
import { UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
|
||||
import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { PageLayoutResetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-reset.service';
|
||||
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
@@ -34,6 +36,7 @@ export class PageLayoutResolver {
|
||||
constructor(
|
||||
private readonly pageLayoutService: PageLayoutService,
|
||||
private readonly pageLayoutUpdateService: PageLayoutUpdateService,
|
||||
private readonly pageLayoutResetService: PageLayoutResetService,
|
||||
) {}
|
||||
|
||||
@Query(() => [PageLayoutDTO])
|
||||
@@ -121,4 +124,16 @@ export class PageLayoutResolver {
|
||||
input,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => PageLayoutWidgetDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
|
||||
async resetPageLayoutWidgetToDefault(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<PageLayoutWidgetDTO> {
|
||||
return this.pageLayoutResetService.resetPageLayoutWidgetToDefault({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
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 { splitEntitiesByResetStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util';
|
||||
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';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
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 { 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';
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutResetService {
|
||||
private readonly logger = new Logger(PageLayoutResetService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly dashboardSyncService: DashboardSyncService,
|
||||
) {}
|
||||
|
||||
async resetPageLayoutWidgetToDefault({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<PageLayoutWidgetDTO> {
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const widget = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(widget) || isDefined(widget.deletedAt)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isFlatPageLayoutWidgetConfigurationOfType(
|
||||
widget,
|
||||
WidgetConfigurationType.FIELDS,
|
||||
)
|
||||
) {
|
||||
throw new PageLayoutWidgetException(
|
||||
`Widget "${id}" is not a FIELDS widget and cannot be reset to default`,
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
if (
|
||||
widget.applicationUniversalIdentifier ===
|
||||
workspaceCustomFlatApplication.universalIdentifier
|
||||
) {
|
||||
throw new PageLayoutWidgetException(
|
||||
`Custom widget "${id}" cannot be reset to default`,
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const widgetToUpdate: FlatPageLayoutWidget = {
|
||||
...widget,
|
||||
overrides: null,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const viewId = widget.configuration.viewId;
|
||||
|
||||
const {
|
||||
viewFieldGroupsToUpdate,
|
||||
viewFieldGroupsToDelete,
|
||||
viewFieldsToUpdate,
|
||||
viewFieldsToDelete,
|
||||
} = isDefined(viewId)
|
||||
? this.computeFieldsWidgetChildResetOperations({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
now,
|
||||
})
|
||||
: {
|
||||
viewFieldGroupsToUpdate: [],
|
||||
viewFieldGroupsToDelete: [],
|
||||
viewFieldsToUpdate: [],
|
||||
viewFieldsToDelete: [],
|
||||
};
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [widgetToUpdate],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
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 widget to default',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatPageLayoutWidgetMaps: recomputedWidgetMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutWidgetMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const updatedWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedWidgetMaps,
|
||||
});
|
||||
|
||||
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByWidgetId({
|
||||
widgetId: id,
|
||||
workspaceId,
|
||||
updatedAt: new Date(updatedWidget.updatedAt),
|
||||
});
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(updatedWidget);
|
||||
}
|
||||
|
||||
private computeFieldsWidgetChildResetOperations({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
viewId: string;
|
||||
flatViewFieldGroupMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatViewFieldGroup | undefined>;
|
||||
};
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatViewField | undefined>;
|
||||
};
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): {
|
||||
viewFieldGroupsToUpdate: FlatViewFieldGroup[];
|
||||
viewFieldGroupsToDelete: FlatViewFieldGroup[];
|
||||
viewFieldsToUpdate: FlatViewField[];
|
||||
viewFieldsToDelete: FlatViewField[];
|
||||
} {
|
||||
const existingGroups = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(group) => group.viewId === viewId && !isDefined(group.deletedAt),
|
||||
);
|
||||
|
||||
const existingFields = Object.values(
|
||||
flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(field) => field.viewId === viewId && !isDefined(field.deletedAt),
|
||||
);
|
||||
|
||||
const { toHardDelete: groupsToDelete, toReset: groupsToReset } =
|
||||
splitEntitiesByResetStrategy({
|
||||
entities: existingGroups,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
const { toHardDelete: fieldsToDelete, toReset: fieldsToReset } =
|
||||
splitEntitiesByResetStrategy({
|
||||
entities: existingFields,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
const viewFieldsToReset = fieldsToReset.map((field) => ({
|
||||
...field,
|
||||
universalOverrides: null,
|
||||
}));
|
||||
|
||||
return {
|
||||
viewFieldGroupsToUpdate: groupsToReset,
|
||||
viewFieldGroupsToDelete: groupsToDelete,
|
||||
viewFieldsToUpdate: viewFieldsToReset,
|
||||
viewFieldsToDelete: fieldsToDelete,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user