Backoffice: Pre-expand tree to target entity when opening Duplicate and Move To modals (closes #22015) (#23063)
* Support tree expansion in generic Duplicate To modal * Add expansion prop to tree picker modal types * Apply expansion from data to picker context * move to action: populate tree picker expansion with ancestors * Pass tree expansion to duplicate document modal * Extract ancestor fetching into private method * Use UmbDocumentTreeRepository directly * Exclude self from ancestor results * make name more explicit * Guard ancestor fetch and simplify expansion * Only set treeExpansion when ancestors exist * fix type issues * Parallelize ancestor and pickable filter fetch * Use getter for treeExpansion; remove unused imports
This commit is contained in:
+20
@@ -1,21 +1,26 @@
|
||||
import { UMB_DUPLICATE_TO_MODAL } from './modal/duplicate-to-modal.token.js';
|
||||
import type { MetaEntityActionDuplicateToKind, UmbDuplicateToRepository } from './types.js';
|
||||
import type { UmbTreeRepository } from '../../data/tree-repository.interface.js';
|
||||
import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { umbOpenModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { linkEntityExpansionEntries } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export class UmbDuplicateToEntityAction extends UmbEntityActionBase<MetaEntityActionDuplicateToKind> {
|
||||
override async execute() {
|
||||
if (!this.args.unique) throw new Error('Unique is not available');
|
||||
if (!this.args.entityType) throw new Error('Entity Type is not available');
|
||||
|
||||
const ancestors = await this.#requestAncestors();
|
||||
|
||||
const value = await umbOpenModal(this, UMB_DUPLICATE_TO_MODAL, {
|
||||
data: {
|
||||
unique: this.args.unique,
|
||||
entityType: this.args.entityType,
|
||||
treeAlias: this.args.meta.treeAlias,
|
||||
foldersOnly: this.args.meta.foldersOnly,
|
||||
treeExpansion: ancestors.length ? linkEntityExpansionEntries(ancestors) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,6 +45,21 @@ export class UmbDuplicateToEntityAction extends UmbEntityActionBase<MetaEntityAc
|
||||
this.#reloadMenu();
|
||||
}
|
||||
|
||||
async #requestAncestors() {
|
||||
try {
|
||||
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository>(this, this.args.meta.treeRepositoryAlias);
|
||||
const { data } =
|
||||
(await treeRepository?.requestTreeItemAncestors({
|
||||
treeItem: { unique: this.args.unique!, entityType: this.args.entityType! },
|
||||
})) ?? {};
|
||||
// Exclude self — the API returns the descendant as part of the ancestors list, but we only want to expand its parents.
|
||||
return data?.filter((item) => item.unique !== this.args.unique) ?? [];
|
||||
} catch {
|
||||
// Tree pre-expansion is a UX convenience — if it fails the modal still opens normally.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async #reloadMenu() {
|
||||
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
|
||||
if (!actionEventContext) throw new Error('Action event context is not available');
|
||||
|
||||
+9
-1
@@ -11,6 +11,10 @@ export class UmbDuplicateToModalElement extends UmbModalBaseElement<UmbDuplicate
|
||||
@state()
|
||||
private _destinationUnique?: string | null;
|
||||
|
||||
private get _treeExpansion() {
|
||||
return this.data?.treeExpansion ?? [];
|
||||
}
|
||||
|
||||
#onTreeSelectionChange(event: UmbSelectionChangeEvent) {
|
||||
const target = event.target as UmbTreeElement;
|
||||
const selection = target.getSelection();
|
||||
@@ -32,6 +36,7 @@ export class UmbDuplicateToModalElement extends UmbModalBaseElement<UmbDuplicate
|
||||
.props=${{
|
||||
foldersOnly: this.data?.foldersOnly,
|
||||
expandTreeRoot: true,
|
||||
expansion: this._treeExpansion,
|
||||
}}
|
||||
@selection-change=${this.#onTreeSelectionChange}></umb-tree>
|
||||
</uui-box>
|
||||
@@ -43,7 +48,10 @@ export class UmbDuplicateToModalElement extends UmbModalBaseElement<UmbDuplicate
|
||||
|
||||
#renderActions() {
|
||||
return html`
|
||||
<uui-button slot="actions" label=${this.localize.term('general_cancel')} @click="${this._rejectModal}"></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
label=${this.localize.term('general_cancel')}
|
||||
@click="${this._rejectModal}"></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
color="positive"
|
||||
|
||||
+2
@@ -1,11 +1,13 @@
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbEntityExpansionModel } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const UMB_DUPLICATE_TO_MODAL_ALIAS = 'Umb.Modal.DuplicateTo';
|
||||
|
||||
export interface UmbDuplicateToModalData extends UmbEntityModel {
|
||||
treeAlias: string;
|
||||
foldersOnly?: boolean;
|
||||
treeExpansion?: UmbEntityExpansionModel;
|
||||
}
|
||||
|
||||
export interface UmbDuplicateToModalValue {
|
||||
|
||||
+24
-1
@@ -1,11 +1,13 @@
|
||||
import { UMB_TREE_PICKER_MODAL } from '../../tree-picker-modal/index.js';
|
||||
import type { UmbTreeItemModel } from '../../types.js';
|
||||
import type { UmbTreeRepository } from '../../data/tree-repository.interface.js';
|
||||
import type { UmbMoveRepository } from './move-repository.interface.js';
|
||||
import type { MetaEntityActionMoveToKind } from './types.js';
|
||||
import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { umbOpenModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { linkEntityExpansionEntries } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export class UmbMoveToEntityAction extends UmbEntityActionBase<MetaEntityActionMoveToKind> {
|
||||
protected async _getPickableFilter(unique: string): Promise<((item: UmbTreeItemModel) => boolean) | undefined> {
|
||||
@@ -16,12 +18,18 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase<MetaEntityActionM
|
||||
if (!this.args.unique) throw new Error('Unique is not available');
|
||||
if (!this.args.entityType) throw new Error('Entity Type is not available');
|
||||
|
||||
const [ancestors, pickableFilter] = await Promise.all([
|
||||
this.#requestAncestors(),
|
||||
this._getPickableFilter(this.args.unique),
|
||||
]);
|
||||
|
||||
const value = await umbOpenModal(this, UMB_TREE_PICKER_MODAL, {
|
||||
data: {
|
||||
treeAlias: this.args.meta.treeAlias,
|
||||
foldersOnly: this.args.meta.foldersOnly,
|
||||
expandTreeRoot: true,
|
||||
pickableFilter: await this._getPickableFilter(this.args.unique),
|
||||
treeExpansion: ancestors.length ? linkEntityExpansionEntries(ancestors) : undefined,
|
||||
pickableFilter,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,6 +51,21 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase<MetaEntityActionM
|
||||
this.#reloadMenu();
|
||||
}
|
||||
|
||||
async #requestAncestors() {
|
||||
try {
|
||||
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository>(this, this.args.meta.treeRepositoryAlias);
|
||||
const { data } =
|
||||
(await treeRepository?.requestTreeItemAncestors({
|
||||
treeItem: { unique: this.args.unique!, entityType: this.args.entityType! },
|
||||
})) ?? {};
|
||||
// Exclude self — the API returns the descendant as part of the ancestors list, but we only want to expand its parents.
|
||||
return data?.filter((item) => item.unique !== this.args.unique) ?? [];
|
||||
} catch {
|
||||
// Tree pre-expansion is a UX convenience — if it fails the modal still opens normally.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async #reloadMenu() {
|
||||
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
|
||||
if (!actionEventContext) throw new Error('Action Event Context is not available');
|
||||
|
||||
+4
@@ -75,6 +75,10 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
|
||||
...this._selectionConfiguration,
|
||||
multiple,
|
||||
};
|
||||
|
||||
if (this.data?.treeExpansion !== undefined) {
|
||||
this._pickerContext.expansion.setExpansion(this.data.treeExpansion);
|
||||
}
|
||||
}
|
||||
|
||||
if (_changedProperties.has('value')) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UmbTreeItemModel, UmbTreeStartNode } from '../types.js';
|
||||
import type { UmbPathPattern, UmbPathPatternParamsType } from '@umbraco-cms/backoffice/router';
|
||||
import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbWorkspaceModalData } from '@umbraco-cms/backoffice/workspace';
|
||||
import type { UmbEntityExpansionModel } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export interface UmbTreePickerModalCreateActionData<PathPatternParamsType extends UmbPathPatternParamsType> {
|
||||
label: string;
|
||||
@@ -17,6 +18,7 @@ export interface UmbTreePickerModalData<
|
||||
> extends UmbPickerModalData<TreeItemType> {
|
||||
hideTreeRoot?: boolean;
|
||||
expandTreeRoot?: boolean;
|
||||
treeExpansion?: UmbEntityExpansionModel;
|
||||
treeAlias?: string;
|
||||
// TODO: create action should be replaces by entity actions in the pickers. Then we also open up for creating folders, choosing where to place items etc. [MR]
|
||||
createAction?: UmbTreePickerModalCreateActionData<PathPatternParamsType>;
|
||||
|
||||
+21
-1
@@ -2,6 +2,7 @@ import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../../e
|
||||
import { UmbDocumentItemRepository } from '../../item/index.js';
|
||||
import { UMB_DUPLICATE_DOCUMENT_MODAL } from './modal/index.js';
|
||||
import { UmbDuplicateDocumentRepository } from './repository/index.js';
|
||||
import { UmbDocumentTreeRepository } from '../../tree/index.js';
|
||||
import { umbOpenModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { UmbEntityActionBase, UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
UmbDocumentTypeDetailRepository,
|
||||
UmbDocumentTypeStructureRepository,
|
||||
} from '@umbraco-cms/backoffice/document-type';
|
||||
import { linkEntityExpansionEntries } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export class UmbDuplicateDocumentEntityAction extends UmbEntityActionBase<never> {
|
||||
override async execute() {
|
||||
@@ -16,13 +18,17 @@ export class UmbDuplicateDocumentEntityAction extends UmbEntityActionBase<never>
|
||||
if (!this.args.entityType) throw new Error('Entity Type is not available');
|
||||
|
||||
const duplicateRepository = new UmbDuplicateDocumentRepository(this);
|
||||
const selectableFilter = await this.#getSelectableFilterByDocumentUnique(this.args.unique);
|
||||
const [selectableFilter, ancestors] = await Promise.all([
|
||||
this.#getSelectableFilterByDocumentUnique(this.args.unique),
|
||||
this.#requestAncestors(),
|
||||
]);
|
||||
|
||||
const value = await umbOpenModal(this, UMB_DUPLICATE_DOCUMENT_MODAL, {
|
||||
data: {
|
||||
unique: this.args.unique,
|
||||
entityType: this.args.entityType,
|
||||
selectableFilter,
|
||||
treeExpansion: ancestors.length ? linkEntityExpansionEntries(ancestors) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,6 +49,20 @@ export class UmbDuplicateDocumentEntityAction extends UmbEntityActionBase<never>
|
||||
this.#reloadMenu(destinationUnique);
|
||||
}
|
||||
|
||||
async #requestAncestors() {
|
||||
try {
|
||||
const treeRepository = new UmbDocumentTreeRepository(this);
|
||||
const { data } = await treeRepository.requestTreeItemAncestors({
|
||||
treeItem: { unique: this.args.unique!, entityType: this.args.entityType! },
|
||||
});
|
||||
// Exclude self — the API returns the descendant as part of the ancestors list, but we only want to expand its parents.
|
||||
return data?.filter((item) => item.unique !== this.args.unique) ?? [];
|
||||
} catch {
|
||||
// Tree pre-expansion is a UX convenience — if it fails the modal still opens normally.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async #getSelectableFilterByDocumentUnique(documentUnique: string) {
|
||||
// 1. Get the document to find its type
|
||||
const itemRepository = new UmbDocumentItemRepository(this);
|
||||
|
||||
+12
-2
@@ -21,6 +21,10 @@ export class UmbDocumentDuplicateToModalElement extends UmbModalBaseElement<
|
||||
@state()
|
||||
private _destinationUnique?: string | null;
|
||||
|
||||
private get _treeExpansion() {
|
||||
return this.data?.treeExpansion ?? [];
|
||||
}
|
||||
|
||||
#onTreeSelectionChange(event: UmbSelectionChangeEvent) {
|
||||
const target = event.target as UmbTreeElement;
|
||||
const selection = target.getSelection();
|
||||
@@ -58,11 +62,14 @@ export class UmbDocumentDuplicateToModalElement extends UmbModalBaseElement<
|
||||
expandTreeRoot: true,
|
||||
hideTreeItemActions: true,
|
||||
selectableFilter: this.#selectableFilter,
|
||||
expansion: this._treeExpansion,
|
||||
}}
|
||||
@selection-change=${this.#onTreeSelectionChange}></umb-tree>
|
||||
</uui-box>
|
||||
<uui-box headline=${this.localize.term('general_options')}>
|
||||
<umb-property-layout label=${this.localize.term('defaultdialogs_relateToOriginalLabel')} orientation="vertical"
|
||||
<umb-property-layout
|
||||
label=${this.localize.term('defaultdialogs_relateToOriginalLabel')}
|
||||
orientation="vertical"
|
||||
><div slot="editor">
|
||||
<uui-toggle
|
||||
@change=${this.#onRelateToOriginalChange}
|
||||
@@ -85,7 +92,10 @@ export class UmbDocumentDuplicateToModalElement extends UmbModalBaseElement<
|
||||
|
||||
#renderActions() {
|
||||
return html`
|
||||
<uui-button slot="actions" label=${this.localize.term('general_cancel')} @click="${this._rejectModal}"></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
label=${this.localize.term('general_cancel')}
|
||||
@click="${this._rejectModal}"></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
color="positive"
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@ import { UMB_DUPLICATE_DOCUMENT_MODAL_ALIAS } from './manifests.js';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbDocumentTreeItemModel } from '../../../types.js';
|
||||
import type { UmbEntityExpansionModel } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UmbDuplicateDocumentModalData extends UmbEntityModel {
|
||||
selectableFilter?: (item: UmbDocumentTreeItemModel) => boolean;
|
||||
treeExpansion?: UmbEntityExpansionModel;
|
||||
}
|
||||
|
||||
export interface UmbDuplicateDocumentModalValue {
|
||||
|
||||
Reference in New Issue
Block a user