Collection rendering performance improvements Part 1: Improve Entity actions render performance (#19605)
* Add null checks for editPath and name in render method The render method now checks for the presence of both editPath and _name before rendering the button, preventing potential errors when these values are missing. * Refactor dropdown open state handling Replaces the public 'open' property with a private field and getter/setter to better control dropdown state. Moves popover open/close logic into the setter, removes the 'updated' lifecycle method, and conditionally renders dropdown content based on the open state. * add opened and closed events * dispatch opened and closed events * Render dropdown content only when open Introduces an _isOpen state to control rendering of the dropdown content in UmbEntityActionsBundleElement. Dropdown content is now only rendered when the dropdown is open, improving performance and preventing unnecessary DOM updates. * Update dropdown.element.ts * create a cache elements * Optimize entity actions observation with IntersectionObserver Adds an IntersectionObserver to only observe entity actions when the element is in the viewport, improving performance. Refactors element creation to use constructors, updates event handling, and ensures cleanup in disconnectedCallback. * only observe once * Update entity-actions-bundle.element.ts * Update dropdown.element.ts * Update entity-actions-bundle.element.ts * split dropdown component * pass compact prop * fix label * Update entity-actions-dropdown.element.ts * Update entity-actions-dropdown.element.ts --------- Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
This commit is contained in:
@@ -5,15 +5,28 @@ import type {
|
||||
UUIPopoverContainerElement,
|
||||
} from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, property, query, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbClosedEvent, UmbOpenedEvent } from '@umbraco-cms/backoffice/event';
|
||||
|
||||
// TODO: maybe move this to UI Library.
|
||||
@customElement('umb-dropdown')
|
||||
export class UmbDropdownElement extends UmbLitElement {
|
||||
#open = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
public get open() {
|
||||
return this.#open;
|
||||
}
|
||||
public set open(value) {
|
||||
this.#open = value;
|
||||
|
||||
if (value === true && this.popoverContainerElement) {
|
||||
this.openDropdown();
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
@property()
|
||||
label?: string;
|
||||
@@ -36,29 +49,12 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
@query('#dropdown-popover')
|
||||
popoverContainerElement?: UUIPopoverContainerElement;
|
||||
|
||||
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.updated(_changedProperties);
|
||||
if (_changedProperties.has('open') && this.popoverContainerElement) {
|
||||
if (this.open) {
|
||||
this.openDropdown();
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#onToggle(event: ToggleEvent) {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.open = event.newState === 'open';
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement?.showPopover();
|
||||
this.#open = true;
|
||||
}
|
||||
|
||||
closeDropdown() {
|
||||
@@ -66,6 +62,20 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement?.hidePopover();
|
||||
this.#open = false;
|
||||
}
|
||||
|
||||
#onToggle(event: ToggleEvent) {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.open = event.newState === 'open';
|
||||
|
||||
if (this.open) {
|
||||
this.dispatchEvent(new UmbOpenedEvent());
|
||||
} else {
|
||||
this.dispatchEvent(new UmbClosedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -81,7 +91,7 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
<slot name="label"></slot>
|
||||
${when(
|
||||
!this.hideExpand,
|
||||
() => html`<uui-symbol-expand id="symbol-expand" .open=${this.open}></uui-symbol-expand>`,
|
||||
() => html`<uui-symbol-expand id="symbol-expand" .open=${this.#open}></uui-symbol-expand>`,
|
||||
)}
|
||||
</uui-button>
|
||||
<uui-popover-container id="dropdown-popover" .placement=${this.placement} @toggle=${this.#onToggle}>
|
||||
@@ -97,6 +107,7 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
css`
|
||||
#dropdown-button {
|
||||
min-width: max-content;
|
||||
height: 100%;
|
||||
}
|
||||
:host(:not([hide-expand]):not([compact])) #dropdown-button {
|
||||
--uui-button-padding-right-factor: 2;
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { UmbEntityContext } from '../../entity/entity.context.js';
|
||||
import type { UmbDropdownElement } from '../dropdown/index.js';
|
||||
import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
ifDefined,
|
||||
css,
|
||||
query,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
@@ -39,11 +29,32 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
@state()
|
||||
private _firstActionHref?: string;
|
||||
|
||||
@query('#action-modal')
|
||||
private _dropdownElement?: UmbDropdownElement;
|
||||
|
||||
// TODO: provide the entity context on a higher level, like the root element of this entity, tree-item/workspace/... [NL]
|
||||
#entityContext = new UmbEntityContext(this);
|
||||
#inViewport = false;
|
||||
#observingEntityActions = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Only observe entity actions when the element is in the viewport
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.#inViewport = true;
|
||||
this.#observeEntityActions();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
root: null, // Use the viewport as the root
|
||||
threshold: 0.1, // Trigger when at least 10% of the element is visible
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(this);
|
||||
}
|
||||
|
||||
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
if (_changedProperties.has('entityType') && _changedProperties.has('unique')) {
|
||||
@@ -54,6 +65,11 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#observeEntityActions() {
|
||||
if (!this.entityType) return;
|
||||
if (this.unique === undefined) return;
|
||||
if (!this.#inViewport) return; // Only observe if the element is in the viewport
|
||||
if (this.#observingEntityActions) return;
|
||||
|
||||
new UmbExtensionsManifestInitializer(
|
||||
this,
|
||||
umbExtensionsRegistry,
|
||||
@@ -67,6 +83,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
},
|
||||
'umbEntityActionsObserver',
|
||||
);
|
||||
|
||||
this.#observingEntityActions = true;
|
||||
}
|
||||
|
||||
async #createFirstActionApi() {
|
||||
@@ -90,14 +108,6 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
await this._firstActionApi?.execute().catch(() => {});
|
||||
}
|
||||
|
||||
#onActionExecuted() {
|
||||
this._dropdownElement?.closeDropdown();
|
||||
}
|
||||
|
||||
#onDropdownClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._numberOfActions === 0) return nothing;
|
||||
return html`<uui-action-bar slot="actions">${this.#renderMore()} ${this.#renderFirstAction()} </uui-action-bar>`;
|
||||
@@ -107,16 +117,9 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
if (this._numberOfActions === 1) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-dropdown id="action-modal" @click=${this.#onDropdownClick} .label=${this.label} compact hide-expand>
|
||||
<uui-symbol-more slot="label" .label=${this.label}></uui-symbol-more>
|
||||
<uui-scroll-container>
|
||||
<umb-entity-action-list
|
||||
@action-executed=${this.#onActionExecuted}
|
||||
.entityType=${this.entityType}
|
||||
.unique=${this.unique}
|
||||
.label=${this.label}></umb-entity-action-list>
|
||||
</uui-scroll-container>
|
||||
</umb-dropdown>
|
||||
<umb-entity-actions-dropdown .label=${this.label} compact>
|
||||
<uui-symbol-more slot="label"></uui-symbol-more>
|
||||
</umb-entity-actions-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { UmbDropdownElement } from '../../../components/dropdown/index.js';
|
||||
import { UmbEntityActionListElement } from '../../entity-action-list.element.js';
|
||||
import { html, customElement, property, css, query } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UUIScrollContainerElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UMB_ENTITY_CONTEXT, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
@customElement('umb-entity-actions-dropdown')
|
||||
export class UmbEntityActionsDropdownElement extends UmbLitElement {
|
||||
@property({ type: Boolean })
|
||||
compact = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@query('#action-modal')
|
||||
private _dropdownElement?: UmbDropdownElement;
|
||||
|
||||
#scrollContainerElement?: UUIScrollContainerElement;
|
||||
#entityActionListElement?: UmbEntityActionListElement;
|
||||
#entityType?: UmbEntityModel['entityType'];
|
||||
#unique?: UmbEntityModel['unique'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_ENTITY_CONTEXT, (context) => {
|
||||
if (!context) return;
|
||||
|
||||
this.observe(observeMultiple([context.entityType, context.unique]), ([entityType, unique]) => {
|
||||
this.#entityType = entityType;
|
||||
this.#unique = unique;
|
||||
|
||||
if (this.#entityActionListElement) {
|
||||
this.#entityActionListElement.entityType = entityType;
|
||||
this.#entityActionListElement.unique = unique;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#onActionExecuted() {
|
||||
this._dropdownElement?.closeDropdown();
|
||||
}
|
||||
|
||||
#onDropdownClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
#onDropdownOpened() {
|
||||
if (this.#scrollContainerElement) {
|
||||
return; // Already created
|
||||
}
|
||||
|
||||
// First create dropdown content when the dropdown is opened.
|
||||
// Programmatically create the elements so they are cached if the dropdown is opened again
|
||||
this.#scrollContainerElement = new UUIScrollContainerElement();
|
||||
this.#entityActionListElement = new UmbEntityActionListElement();
|
||||
this.#entityActionListElement.addEventListener('action-executed', this.#onActionExecuted);
|
||||
this.#entityActionListElement.entityType = this.#entityType;
|
||||
this.#entityActionListElement.unique = this.#unique;
|
||||
this.#entityActionListElement.setAttribute('label', this.label ?? '');
|
||||
this.#scrollContainerElement.appendChild(this.#entityActionListElement);
|
||||
this._dropdownElement?.appendChild(this.#scrollContainerElement);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<umb-dropdown
|
||||
id="action-modal"
|
||||
@click=${this.#onDropdownClick}
|
||||
@opened=${this.#onDropdownOpened}
|
||||
.label=${this.label}
|
||||
?compact=${this.compact}
|
||||
hide-expand>
|
||||
<slot name="label" slot="label"></slot>
|
||||
<slot></slot>
|
||||
</umb-dropdown>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
uui-scroll-container {
|
||||
max-height: 700px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-entity-actions-dropdown': UmbEntityActionsDropdownElement;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
import './entity-actions-dropdown/entity-actions-dropdown.element.js';
|
||||
import './entity-actions-table-column-view/entity-actions-table-column-view.element.js';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export class UmbClosedEvent extends Event {
|
||||
public static readonly TYPE = 'closed';
|
||||
|
||||
public constructor() {
|
||||
// mimics the native toggle event
|
||||
super(UmbClosedEvent.TYPE, { bubbles: false, composed: false, cancelable: false });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export * from './action-executed.event.js';
|
||||
export * from './change.event.js';
|
||||
export * from './closed.event.js';
|
||||
export * from './delete.event.js';
|
||||
export * from './deselected.event.js';
|
||||
export * from './input.event.js';
|
||||
export * from './opened.event.js';
|
||||
export * from './progress.event.js';
|
||||
export * from './selected.event.js';
|
||||
export * from './selection-change.event.js';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export class UmbOpenedEvent extends Event {
|
||||
public static readonly TYPE = 'opened';
|
||||
|
||||
public constructor() {
|
||||
// mimics the native toggle event
|
||||
super(UmbOpenedEvent.TYPE, { bubbles: false, composed: false, cancelable: false });
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '../../contexts/index.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, state, nothing, query } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { html, customElement, state, nothing, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
@customElement('umb-workspace-entity-action-menu')
|
||||
export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement {
|
||||
private _workspaceContext?: typeof UMB_ENTITY_WORKSPACE_CONTEXT.TYPE;
|
||||
@@ -15,12 +13,6 @@ export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement {
|
||||
@state()
|
||||
private _entityType?: string;
|
||||
|
||||
@state()
|
||||
private _popoverOpen = false;
|
||||
|
||||
@query('#workspace-entity-action-menu-popover')
|
||||
private _popover?: UUIPopoverContainerElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -35,48 +27,16 @@ export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement {
|
||||
});
|
||||
}
|
||||
|
||||
#onActionExecuted(event: UmbActionExecutedEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this._popover?.hidePopover();
|
||||
}
|
||||
|
||||
#onPopoverToggle(event: ToggleEvent) {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this._popoverOpen = event.newState === 'open';
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._unique !== undefined && this._entityType
|
||||
? html`
|
||||
<uui-button
|
||||
id="action-button"
|
||||
data-mark="workspace:action-menu-button"
|
||||
popovertarget="workspace-entity-action-menu-popover"
|
||||
label=${this.localize.term('general_actions')}>
|
||||
<uui-symbol-more></uui-symbol-more>
|
||||
</uui-button>
|
||||
<uui-popover-container
|
||||
id="workspace-entity-action-menu-popover"
|
||||
placement="bottom-end"
|
||||
@toggle=${this.#onPopoverToggle}>
|
||||
<umb-popover-layout>
|
||||
<uui-scroll-container>
|
||||
<umb-entity-action-list
|
||||
@action-executed=${this.#onActionExecuted}
|
||||
.entityType=${this._entityType}
|
||||
.unique=${this._unique}>
|
||||
</umb-entity-action-list>
|
||||
</uui-scroll-container>
|
||||
</umb-popover-layout>
|
||||
</uui-popover-container>
|
||||
`
|
||||
: nothing;
|
||||
if (!this._entityType) return nothing;
|
||||
if (this._unique === undefined) return nothing;
|
||||
|
||||
return html`<umb-entity-actions-dropdown>
|
||||
<uui-symbol-more
|
||||
slot="label"
|
||||
data-mark="workspace:action-menu-button"
|
||||
label=${this.localize.term('general_actions')}></uui-symbol-more>
|
||||
</umb-entity-actions-dropdown>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
@@ -86,7 +46,8 @@ export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement {
|
||||
height: 100%;
|
||||
margin-left: calc(var(--uui-size-layout-1) * -1);
|
||||
}
|
||||
:host > uui-button {
|
||||
|
||||
umb-entity-actions-dropdown {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -34,7 +34,9 @@ export class UmbDocumentTableColumnNameElement extends UmbLitElement implements
|
||||
|
||||
override render() {
|
||||
if (!this.value) return nothing;
|
||||
return html` <uui-button compact href=${this.value.editPath} label=${this._name}></uui-button> `;
|
||||
if (!this.value.editPath) return nothing;
|
||||
if (!this._name) return nothing;
|
||||
return html`<uui-button compact href=${this.value.editPath} label=${this._name}></uui-button>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.36",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.25",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.27",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
@@ -66,9 +66,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "16.0.25",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.25.tgz",
|
||||
"integrity": "sha512-IvRkkrTIxlXbg2dw0RhAUgkb7KSBJCyktK6zJynOORgZ5RXRae19hqKk7yEu2EwJpTstl6m9AzoVf1x4b94x5w==",
|
||||
"version": "16.0.27",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.27.tgz",
|
||||
"integrity": "sha512-KxjIpfFsiK5b1Au8QrlWceK88eo53VxogLs0LMrxsRS3rt4rdmD1YRP6U+yIucdPKnhVgfIsh40J/taGAZyPFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.36",
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.36",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.25",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.27",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
@@ -213,6 +213,7 @@ test('can create a document type with a composition', {tag: '@smoke'}, async ({u
|
||||
|
||||
// Act
|
||||
await umbracoUi.documentType.goToDocumentType(documentTypeName);
|
||||
await umbracoUi.waitForTimeout(1000);
|
||||
await umbracoUi.documentType.clickCompositionsButton();
|
||||
await umbracoUi.documentType.clickModalMenuItemWithName(compositionDocumentTypeName);
|
||||
await umbracoUi.documentType.clickSubmitButton();
|
||||
|
||||
Reference in New Issue
Block a user