Document/Media Recycle Bin: Show full breadcrumb (#20547)

* register structure context for recycle bin

* Update manifests.ts

* export consts

* move href construction to context + override for document and media
This commit is contained in:
Mads Rasmussen
2025-10-20 13:43:10 +02:00
committed by GitHub
parent ab4be79da0
commit 94692cccb7
19 changed files with 197 additions and 77 deletions

View File

@@ -4,4 +4,5 @@ import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface UmbMenuVariantStructureWorkspaceContext extends UmbContext {
structure: Observable<UmbVariantStructureItemModel[]>;
getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined;
}

View File

@@ -7,9 +7,14 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import {
UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT,
UMB_VARIANT_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/workspace';
import { linkEntityExpansionEntries } from '@umbraco-cms/backoffice/utils';
import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs {
treeRepositoryAlias: string;
@@ -31,11 +36,15 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
*/
public readonly parent = this.#parent.asObservable();
protected _sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE;
#parentContext = new UmbParentEntityContext(this);
#ancestorContext = new UmbAncestorsEntityContext(this);
#sectionSidebarMenuContext?: typeof UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT.TYPE;
#isModalContext: boolean = false;
#isNew: boolean | undefined = undefined;
#variantWorkspaceContext?: typeof UMB_VARIANT_WORKSPACE_CONTEXT.TYPE;
#workspaceActiveVariantId?: UmbVariantId;
public readonly IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT = true;
@@ -49,6 +58,16 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
this.#isModalContext = modalContext !== undefined;
});
this.consumeContext(UMB_SECTION_CONTEXT, (instance) => {
this._sectionContext = instance;
});
this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => {
if (!instance) return;
this.#variantWorkspaceContext = instance;
this.#observeWorkspaceActiveVariant();
});
this.consumeContext(UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT, (instance) => {
this.#sectionSidebarMenuContext = instance;
});
@@ -75,6 +94,10 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
});
}
getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined {
return `section/${this._sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit/${structureItem.unique}/${this.#workspaceActiveVariantId?.toCultureString()}`;
}
async #requestStructure() {
const isNew = this.#workspaceContext?.getIsNew();
const uniqueObservable = isNew
@@ -191,6 +214,19 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
this.#sectionSidebarMenuContext?.expansion.expandItems(expandableItemsWithMenuItem);
}
#observeWorkspaceActiveVariant() {
this.observe(
this.#variantWorkspaceContext?.splitView.activeVariantsInfo,
(value) => {
if (!value) return;
if (value?.length === 0) return;
this.#workspaceActiveVariantId = UmbVariantId.Create(value[0]);
},
'breadcrumbWorkspaceActiveVariantObserver',
);
}
override destroy(): void {
super.destroy();
this.#structure.destroy();

View File

@@ -6,7 +6,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
import { UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/menu';
import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language';
import type { UmbVariantStructureItemModel } from '@umbraco-cms/backoffice/menu';
@@ -24,10 +23,9 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
@state()
private _appDefaultCulture?: string;
#sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE;
#workspaceContext?: UmbVariantDatasetWorkspaceContext;
#appLanguageContext?: UmbAppLanguageContext;
#structureContext?: typeof UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT.TYPE;
#menuStructureContext?: typeof UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
@@ -37,10 +35,6 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
this.#observeDefaultCulture();
});
this.consumeContext(UMB_SECTION_CONTEXT, (instance) => {
this.#sectionContext = instance;
});
this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => {
if (!instance) return;
this.#workspaceContext = instance;
@@ -50,15 +44,15 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
this.consumeContext(UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT, (instance) => {
if (!instance) return;
this.#structureContext = instance;
this.#menuStructureContext = instance;
this.#observeStructure();
});
}
#observeStructure() {
if (!this.#structureContext || !this.#workspaceContext) return;
if (!this.#menuStructureContext || !this.#workspaceContext) return;
this.observe(this.#structureContext.structure, (value) => {
this.observe(this.#menuStructureContext.structure, (value) => {
if (!this.#workspaceContext) return;
const unique = this.#workspaceContext.getUnique();
// exclude the current unique from the structure. We append this with an observer of the name
@@ -113,15 +107,8 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
return structureItem.variants?.[0]?.name ?? '(#general_unknown)';
}
#getHref(structureItem: any) {
if (structureItem.isFolder) return undefined;
let href = `section/${this.#sectionContext?.getPathname()}`;
if (structureItem.unique) {
href += `/workspace/${structureItem.entityType}/edit/${structureItem.unique}/${this._workspaceActiveVariantId?.toCultureString()}`;
}
return href;
#getHref(structureItem: UmbVariantStructureItemModel) {
return this.#menuStructureContext?.getItemHref(structureItem);
}
override render() {

View File

@@ -1,11 +1,23 @@
import { UMB_DOCUMENT_TREE_REPOSITORY_ALIAS } from '../tree/index.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu';
import {
UmbMenuVariantTreeStructureWorkspaceContextBase,
type UmbVariantStructureItemModel,
} from '@umbraco-cms/backoffice/menu';
export class UmbDocumentMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase {
constructor(host: UmbControllerHost) {
super(host, { treeRepositoryAlias: UMB_DOCUMENT_TREE_REPOSITORY_ALIAS });
}
override getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined {
// The Document menu does not have a root item, so we do not have a href for it.
if (!structureItem.unique) {
return `section/${this._sectionContext?.getPathname()}`;
} else {
return super.getItemHref(structureItem);
}
}
}
export default UmbDocumentMenuStructureContext;

View File

@@ -1,3 +1,4 @@
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../constants.js';
import { UMB_DOCUMENT_TREE_ALIAS } from '../tree/index.js';
import { UMB_DOCUMENT_MENU_ITEM_ALIAS } from './constants.js';
import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
@@ -36,7 +37,7 @@ export const manifests: Array<UmbExtensionManifest> = [
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: 'Umb.Workspace.Document',
match: UMB_DOCUMENT_WORKSPACE_ALIAS,
},
{
alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS,
@@ -51,7 +52,7 @@ export const manifests: Array<UmbExtensionManifest> = [
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: 'Umb.Workspace.Document',
match: UMB_DOCUMENT_WORKSPACE_ALIAS,
},
],
},

View File

@@ -1,3 +1,4 @@
export * from './menu/constants.js';
export * from './repository/constants.js';
export * from './tree/constants.js';
export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'document-recycle-bin-root';

View File

@@ -1,5 +1,5 @@
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
@@ -11,7 +11,7 @@ export const manifests: Array<UmbExtensionManifest> = [
api: () => import('./allow-document-recycle-bin.condition.js'),
},
...entityActionManifests,
...menuItemManifests,
...menuManifests,
...repositoryManifests,
...treeManifests,
];

View File

@@ -1,23 +0,0 @@
import { UMB_CONTENT_MENU_ALIAS } from '../../menu/manifests.js';
import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS } from '../constants.js';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'menuItem',
kind: 'tree',
alias: 'Umb.MenuItem.Document.RecycleBin',
name: 'Document Recycle Bin Menu Item',
weight: 100,
meta: {
treeAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS,
label: 'Recycle Bin',
icon: 'icon-trash',
menus: [UMB_CONTENT_MENU_ALIAS],
},
conditions: [
{
alias: 'Umb.Condition.CurrentUser.AllowDocumentRecycleBin',
},
],
},
];

View File

@@ -0,0 +1 @@
export const UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS = 'Umb.MenuItem.Document.RecycleBin';

View File

@@ -0,0 +1,11 @@
import { UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS } from '../tree/constants.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu';
export class UmbDocumentRecycleBinMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase {
constructor(host: UmbControllerHost) {
super(host, { treeRepositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS });
}
}
export { UmbDocumentRecycleBinMenuStructureContext as api };

View File

@@ -0,0 +1,46 @@
import { UMB_CONTENT_MENU_ALIAS } from '../../menu/manifests.js';
import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS } from '../constants.js';
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../constants.js';
import { UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS } from './constants.js';
import { UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'menuItem',
kind: 'tree',
alias: UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS,
name: 'Document Recycle Bin Menu Item',
weight: 100,
meta: {
treeAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS,
label: 'Recycle Bin',
icon: 'icon-trash',
menus: [UMB_CONTENT_MENU_ALIAS],
},
conditions: [
{
alias: 'Umb.Condition.CurrentUser.AllowDocumentRecycleBin',
},
],
},
{
type: 'workspaceContext',
kind: 'menuStructure',
name: 'Document Recycle Bin Menu Structure Workspace Context',
alias: 'Umb.Context.DocumentRecycleBin.Menu.Structure',
api: () => import('./document-recycle-bin-menu-structure.context.js'),
meta: {
menuItemAlias: UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS,
},
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: UMB_DOCUMENT_WORKSPACE_ALIAS,
},
{
alias: UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS,
},
],
},
];

View File

@@ -1,4 +1,4 @@
import { UMB_MEDIA_TREE_ALIAS } from '../constants.js';
import { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_WORKSPACE_ALIAS } from '../constants.js';
import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_MENU_ITEM_ALIAS } from './constants.js';
import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace';
@@ -34,7 +34,7 @@ export const manifests: Array<UmbExtensionManifest> = [
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: 'Umb.Workspace.Media',
match: UMB_MEDIA_WORKSPACE_ALIAS,
},
{
alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS,
@@ -49,7 +49,7 @@ export const manifests: Array<UmbExtensionManifest> = [
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: 'Umb.Workspace.Media',
match: UMB_MEDIA_WORKSPACE_ALIAS,
},
],
},

View File

@@ -1,11 +1,23 @@
import { UMB_MEDIA_TREE_REPOSITORY_ALIAS } from '../constants.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu';
import {
UmbMenuVariantTreeStructureWorkspaceContextBase,
type UmbVariantStructureItemModel,
} from '@umbraco-cms/backoffice/menu';
export class UmbMediaMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase {
constructor(host: UmbControllerHost) {
super(host, { treeRepositoryAlias: UMB_MEDIA_TREE_REPOSITORY_ALIAS });
}
override getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined {
// The Media menu does not have a root item, so we do not have a href for it.
if (!structureItem.unique) {
return `section/${this._sectionContext?.getPathname()}`;
} else {
return super.getItemHref(structureItem);
}
}
}
export default UmbMediaMenuStructureContext;

View File

@@ -1,3 +1,4 @@
export * from './tree/constants.js';
export * from './menu/constants.js';
export * from './repository/constants.js';
export * from './tree/constants.js';
export const UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'media-recycle-bin-root';

View File

@@ -1,5 +1,5 @@
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
@@ -11,7 +11,7 @@ export const manifests: Array<UmbExtensionManifest> = [
api: () => import('./allow-media-recycle-bin.condition.js'),
},
...entityActionManifests,
...menuItemManifests,
...menuManifests,
...repositoryManifests,
...treeManifests,
];

View File

@@ -1,22 +0,0 @@
import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS } from '../../constants.js';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'menuItem',
kind: 'tree',
alias: 'Umb.MenuItem.Media.RecycleBin',
name: 'Media Recycle Bin Menu Item',
weight: 100,
meta: {
treeAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS,
label: 'Recycle Bin',
icon: 'icon-trash',
menus: [UMB_MEDIA_MENU_ALIAS],
},
conditions: [
{
alias: 'Umb.Condition.CurrentUser.AllowMediaRecycleBin',
},
],
},
];

View File

@@ -0,0 +1 @@
export const UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS = 'Umb.MenuItem.Media.RecycleBin';

View File

@@ -0,0 +1,44 @@
import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace';
import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS, UMB_MEDIA_WORKSPACE_ALIAS } from '../../constants.js';
import { UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS } from './constants.js';
import { UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'menuItem',
kind: 'tree',
alias: UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS,
name: 'Media Recycle Bin Menu Item',
weight: 100,
meta: {
treeAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS,
label: 'Recycle Bin',
icon: 'icon-trash',
menus: [UMB_MEDIA_MENU_ALIAS],
},
conditions: [
{
alias: 'Umb.Condition.CurrentUser.AllowMediaRecycleBin',
},
],
},
{
type: 'workspaceContext',
kind: 'menuStructure',
name: 'Media Recycle Bin Menu Structure Workspace Context',
alias: 'Umb.Context.MediaRecycleBin.Menu.Structure',
api: () => import('./media-recycle-bin-menu-structure.context.js'),
meta: {
menuItemAlias: UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS,
},
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: UMB_MEDIA_WORKSPACE_ALIAS,
},
{
alias: UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS,
},
],
},
];

View File

@@ -0,0 +1,11 @@
import { UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS } from '../tree/constants.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu';
export class UmbMediaRecycleBinMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase {
constructor(host: UmbControllerHost) {
super(host, { treeRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS });
}
}
export { UmbMediaRecycleBinMenuStructureContext as api };