Skip to content

Commit e5f6134

Browse files
madsrasmussenandr317cnielslyngsoe
authored
Section Sidebar Menu Expansion (#19810)
* wip section menu expansion * make section context local to each section * split kind manifest from element file * make generic entity expansion manager * wip menu context * add collapsed and expanded events * Export new expansion entity event modules * rename events * dispatch events * Set tree expansion changes in the menu context * expand menu from workspace * do not allow undefined * make menu item feature folder * Update menu-variant-tree-structure-workspace-context-base.ts * menu: pass expansion as prop to prevent dependency on the section sidebar * use correct event * Add event listener support to extension slot element Introduces an 'events' property to UmbExtensionSlotElement, allowing dynamic assignment and removal of event listeners on extension components. Event listeners are added when extensions are permitted and removed on disconnect, improving extensibility and event handling for extension slots. * Add entity expansion event handling to sidebar menu Introduces handlers for entity expansion and collapse events in the section sidebar menu. This change enables the menu to respond to expansion state changes by updating the context accordingly. * Optimize expansion state updates in menu components Introduces a local expansion state to both section sidebar and tree menu item components to prevent unnecessary updates and rerenders. This improves performance by ensuring state updates only occur when needed. * only check if we have a local state already * add bulk expand method * use bulk expand method * align naming * mute updates * lower threshold * add expansion model with target * add function to link entries * fix self import * export constants * update js docs for entity expansion manager * link entries * fix import * do not export from menu here * fix import * fix import * align how we register manifests * add specific managers for section sidebar menu * use structure items * dot not expand current item * Refactor section sidebar menu to use programmatic extension slot Replaces the template-based <umb-extension-slot> with a programmatically created UmbExtensionSlotElement for improved performance and UX. * add section context extension * register menu as section context instead of hardcoding * rename folder * align naming * export extension slot elements * fix typings * destroy extension slot element when host is disconnected * use entry model * move and rename * register global context to hold menu state across sections * temp observe section specific expansions * temp observe section specific expansions * add method to collapse multiple items * bind expansion to section * make entity expansion manager generic * add helper method * remove temp test data * include last item in target * remove unused * pass full entry to event * add type for menu item expansion * export types * add menuItem alias * Update types.ts * support menu item expansion entry * add const for menu item alias + use for breadcrumb and menu item * add data type menu item alias const + apply to breadcrumb * move to correct manifest * add menu item alias to expand entries * Update manifests.ts * add menu structure kind types * add kind to manifests * add menu item context * filter menu items * handle menu item expansion * clean up * fix order * add example dashboard and entity action * import types * align model type names * align naming * use ui component * add guard for menu item entry * use correct type * Update section-sidebar-menu.element.ts * Update entity-expansion.manager.ts * export constants * add menuItemAlias to manifest * add menuItemAlias to manifest * add menuItemAlias to manifest * add menuItemAlias to manifest * add menu item alias * add menuItemAlias to manifest * add menuItemAlias to manifest * add menuItemAlias to manifest * add alias * add kind * fix import path * do not expand menu from modal * collect all menu-item files in one folder * fix lint errors * Update content-detail-workspace-base.ts * clean up * rename to example * add button to collapse everything within a section * fix breadcrumb for non-variant structure * reload entity * destroy * remove self * Updated acceptance tests to check if a caret button is open before clicking * Bumped version of test helpers * use const --------- Co-authored-by: Andreas Zerbst <[email protected]> Co-authored-by: Niels Lyngsø <[email protected]>
1 parent d60be0f commit e5f6134

File tree

138 files changed

+1521
-227
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+1521
-227
lines changed

src/Umbraco.Web.UI.Client/devops/module-dependencies/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createImportMap } from '../importmap/index.js';
44

55
const ILLEGAL_CORE_IMPORTS_THRESHOLD = 5;
66
const SELF_IMPORTS_THRESHOLD = 0;
7-
const BIDIRECTIONAL_IMPORTS_THRESHOLD = 18;
7+
const BIDIRECTIONAL_IMPORTS_THRESHOLD = 16;
88

99
const clientProjectRoot = path.resolve(import.meta.dirname, '../../');
1010
const modulePrefix = '@umbraco-cms/backoffice/';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
2+
import { UMB_MENU_ITEM_CONTEXT } from '@umbraco-cms/backoffice/menu';
3+
4+
export class ExampleCollapseMenuItemEntityAction extends UmbEntityActionBase<never> {
5+
override async execute() {
6+
const context = await this.getContext(UMB_MENU_ITEM_CONTEXT);
7+
context?.expansion.collapseAll();
8+
}
9+
}
10+
11+
export { ExampleCollapseMenuItemEntityAction as api };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const manifests: Array<UmbExtensionManifest> = [
2+
{
3+
type: 'entityAction',
4+
kind: 'default',
5+
alias: 'Umb.EntityAction.Document.MenuItem.Collapse',
6+
name: 'Collapse Document Menu Item',
7+
api: () => import('./collapse-menu-item.entity-action.js'),
8+
forEntityTypes: ['document-type-root', 'media-type-root', 'member-type-root', 'data-type-root'],
9+
weight: -10,
10+
meta: {
11+
label: 'Collapse Menu Item',
12+
icon: 'icon-wand',
13+
},
14+
},
15+
];
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { manifests as playgroundDashboardManifests } from './playground-dashboard/manifests.js';
2+
import { manifests as collapseMenuItemManifests } from './collapse-menu-item-entity-action/manifests.js';
3+
4+
export const manifests: Array<UmbExtensionManifest> = [...playgroundDashboardManifests, ...collapseMenuItemManifests];
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const manifests: Array<UmbExtensionManifest> = [
2+
{
3+
type: 'dashboard',
4+
kind: 'default',
5+
name: 'Example Section Sidebar Menu Playground Dashboard',
6+
alias: 'Example.Dashboard.SectionSidebarMenuPlayground',
7+
element: () => import('./section-sidebar-menu-playground.element.js'),
8+
weight: 3000,
9+
meta: {
10+
label: 'Section Sidebar Menu Playground',
11+
pathname: 'section-sidebar-menu-playground',
12+
},
13+
},
14+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { html, customElement, LitElement, css, state, repeat } from '@umbraco-cms/backoffice/external/lit';
2+
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
3+
import {
4+
UMB_SECTION_SIDEBAR_MENU_GLOBAL_CONTEXT,
5+
UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT,
6+
type UmbSectionMenuItemExpansionEntryModel,
7+
} from '@umbraco-cms/backoffice/menu';
8+
9+
@customElement('example-section-sidebar-menu-playground-dashboard')
10+
export class ExampleSectionSidebarMenuPlaygroundDashboard extends UmbElementMixin(LitElement) {
11+
#globalContext?: typeof UMB_SECTION_SIDEBAR_MENU_GLOBAL_CONTEXT.TYPE;
12+
#sectionContext?: typeof UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT.TYPE;
13+
14+
@state()
15+
private _globalExpansion: Array<UmbSectionMenuItemExpansionEntryModel> = [];
16+
17+
@state()
18+
private _sectionExpansion: Array<UmbSectionMenuItemExpansionEntryModel> = [];
19+
20+
constructor() {
21+
super();
22+
23+
this.consumeContext(UMB_SECTION_SIDEBAR_MENU_GLOBAL_CONTEXT, (section) => {
24+
this.#globalContext = section;
25+
this.#observeGlobalExpansion();
26+
});
27+
28+
this.consumeContext(UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT, (section) => {
29+
this.#sectionContext = section;
30+
this.#observeSectionExpansion();
31+
});
32+
}
33+
34+
#observeGlobalExpansion() {
35+
this.observe(this.#globalContext?.expansion.expansion, (items) => {
36+
this._globalExpansion = items || [];
37+
});
38+
}
39+
40+
#observeSectionExpansion() {
41+
this.observe(this.#sectionContext?.expansion.expansion, (items) => {
42+
this._sectionExpansion = items || [];
43+
});
44+
}
45+
46+
#onCloseItem(event: PointerEvent, item: UmbSectionMenuItemExpansionEntryModel) {
47+
event.stopPropagation();
48+
this.#sectionContext?.expansion.collapseItem(item);
49+
}
50+
51+
#onCollapseSection() {
52+
this.#sectionContext?.expansion.collapseAll();
53+
}
54+
55+
override render() {
56+
return html` <umb-stack>
57+
<uui-box headline="Open Items for this section">
58+
<uui-button slot="header-actions" @click=${this.#onCollapseSection} compact>
59+
<uui-icon name="icon-wand"></uui-icon>
60+
Collapse All</uui-button
61+
>
62+
${repeat(
63+
this._sectionExpansion,
64+
(item) => item.entityType + item.unique,
65+
(item) => this.#renderItem(item),
66+
)}
67+
</uui-box>
68+
<uui-box headline="Open Items for all sections">
69+
${repeat(
70+
this._globalExpansion,
71+
(item) => item.entityType + item.unique,
72+
(item) => this.#renderItem(item),
73+
)}
74+
</uui-box>
75+
</umb-stack>`;
76+
}
77+
78+
#renderItem(item: UmbSectionMenuItemExpansionEntryModel) {
79+
return html` <uui-ref-node
80+
name=${item.entityType}
81+
detail=${item.unique + ', ' + item.menuItemAlias + ', ' + item.sectionAlias}>
82+
<uui-button slot="actions" @click=${(event: PointerEvent) => this.#onCloseItem(event, item)}>Close</uui-button>
83+
</uui-ref-node>`;
84+
}
85+
86+
static override styles = [
87+
css`
88+
:host {
89+
display: block;
90+
padding: var(--uui-size-layout-1);
91+
}
92+
`,
93+
];
94+
}
95+
96+
export { ExampleSectionSidebarMenuPlaygroundDashboard as element };
97+
98+
declare global {
99+
interface HTMLElementTagNameMap {
100+
'example-section-sidebar-menu-playground-dashboard': ExampleSectionSidebarMenuPlaygroundDashboard;
101+
}
102+
}

src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-main.element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class UmbBackofficeMainElement extends UmbLitElement {
7878
this._routes = newRoutes;
7979
}
8080

81+
// TODO: v17. Remove this section context when we have api's as part of the section manifest.
8182
private _provideSectionContext(sectionManifest: ManifestSection) {
8283
if (!this._sectionContext) {
8384
this._sectionContext = new UmbSectionContext(this);

src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,11 +941,20 @@ export abstract class UmbContentDetailWorkspaceContextBase<
941941
if (!eventContext) {
942942
throw new Error('Event context is missing');
943943
}
944-
const event = new UmbRequestReloadChildrenOfEntityEvent({
944+
945+
const reloadStructureEvent = new UmbRequestReloadStructureForEntityEvent({
945946
entityType: parent.entityType,
946947
unique: parent.unique,
947948
});
948-
eventContext.dispatchEvent(event);
949+
950+
eventContext.dispatchEvent(reloadStructureEvent);
951+
952+
const reloadChildrenEvent = new UmbRequestReloadChildrenOfEntityEvent({
953+
entityType: parent.entityType,
954+
unique: parent.unique,
955+
});
956+
957+
eventContext.dispatchEvent(reloadChildrenEvent);
949958
}
950959

951960
async #update(variantIds: Array<UmbVariantId>, saveData: DetailModelType) {

src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/components/extension-slot/extension-slot.element.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ export class UmbExtensionSlotElement extends UmbLitElement {
8888
}
8989
#props?: Record<string, unknown> = {};
9090

91+
@property({ type: Object, attribute: false })
92+
set events(newVal: Record<string, (event: Event) => void> | undefined) {
93+
this.#events = newVal;
94+
if (this.#extensionsController) {
95+
this.#addEventListenersToExtensionElement();
96+
}
97+
}
98+
get events(): Record<string, (event: Event) => void> | undefined {
99+
return this.#events;
100+
}
101+
#events?: Record<string, (event: Event) => void> = {};
102+
91103
@property({ type: String, attribute: 'default-element' })
92104
public defaultElement?: string;
93105

@@ -113,6 +125,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
113125
}
114126
override disconnectedCallback(): void {
115127
// _permitted is reset as the extensionsController fires a callback on destroy.
128+
this.#removeEventListenersFromExtensionElement();
116129
this.#attached = false;
117130
this.#extensionsController?.destroy();
118131
this.#extensionsController = undefined;
@@ -130,6 +143,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
130143
this.filter,
131144
(extensionControllers) => {
132145
this._permitted = extensionControllers;
146+
this.#addEventListenersToExtensionElement();
133147
},
134148
undefined, // We can leave the alias undefined as we destroy this our selfs.
135149
this.defaultElement,
@@ -158,6 +172,36 @@ export class UmbExtensionSlotElement extends UmbLitElement {
158172
return this.renderMethod ? this.renderMethod(ext, i) : ext.component;
159173
};
160174

175+
#addEventListenersToExtensionElement() {
176+
this._permitted?.forEach((initializer) => {
177+
const component = initializer.component as HTMLElement;
178+
if (!component) return;
179+
180+
const events = this.#events;
181+
if (!events) return;
182+
183+
this.#removeEventListenersFromExtensionElement();
184+
185+
Object.entries(events).forEach(([eventName, handler]) => {
186+
component.addEventListener(eventName, handler);
187+
});
188+
});
189+
}
190+
191+
#removeEventListenersFromExtensionElement() {
192+
this._permitted?.forEach((initializer) => {
193+
const component = initializer.component as HTMLElement;
194+
if (!component) return;
195+
196+
const events = this.#events;
197+
if (!events) return;
198+
199+
Object.entries(events).forEach(([eventName, handler]) => {
200+
component.removeEventListener(eventName, handler);
201+
});
202+
});
203+
}
204+
161205
static override styles = css`
162206
:host {
163207
display: contents;

src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export * from './conditions/index.js';
22
export * from './initializers/index.js';
33
export * from './registry.js';
44
export * from './utils/index.js';
5+
export * from './components/index.js';
6+
57
export type * from './models/types.js';
68
export type * from './extensions/types.js';
79

0 commit comments

Comments
 (0)