From 892fa770669fc4de6e23c59bf750a4bb22eb6d0c Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 30 Apr 2024 17:52:27 +0100 Subject: [PATCH 1/8] Media Collection View fixes Aligns with Document Collection View fixes, PR #1737 - Adds loading state - Fixes open media editor modal --- .../media-collection.server.data-source.ts | 3 + .../packages/media/media/collection/types.ts | 9 ++ .../media-grid-collection-view.element.ts | 79 +++++++++++----- .../media-table-column-name.element.ts | 48 ++++------ .../media-table-collection-view.element.ts | 89 +++++++++++++------ 5 files changed, 146 insertions(+), 82 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts index b75fadfff8..19c75cb63a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts @@ -32,12 +32,15 @@ export class UmbMediaCollectionServerDataSource implements UmbCollectionDataSour const model: UmbMediaCollectionItemModel = { unique: item.id, + entityType: 'media', + contentTypeAlias: item.mediaType.alias, createDate: new Date(variant.createDate), creator: item.creator, icon: item.mediaType.icon, name: variant.name, sortOrder: item.sortOrder, updateDate: new Date(variant.updateDate), + updater: item.creator, // TODO: Check if the `updater` is available for media items. [LK] values: item.values.map((item) => { return { alias: item.alias, value: item.value as string }; }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts index 1ae06b64f7..57d968794a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts @@ -10,11 +10,20 @@ export interface UmbMediaCollectionFilterModel extends UmbCollectionFilterModel export interface UmbMediaCollectionItemModel { unique: string; + entityType: string; + contentTypeAlias: string; createDate: Date; creator?: string | null; icon: string; name: string; sortOrder: number; updateDate: Date; + updater?: string | null; values: Array<{ alias: string; value: string }>; } + +export interface UmbEditableMediaCollectionItemModel { + item: UmbMediaCollectionItemModel; + editPath: string; +} + diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index 6a77f11469..da754c7398 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -1,12 +1,16 @@ -import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbMediaCollectionItemModel } from '../../types.js'; +import type { UmbMediaCollectionContext } from '../../media-collection.context.js'; +import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; @customElement('umb-media-grid-collection-view') export class UmbMediaGridCollectionViewElement extends UmbLitElement { + @state() + private _editMediaPath = ''; + @state() private _items: Array = []; @@ -16,7 +20,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { @state() private _selection: Array = []; - #collectionContext?: UmbDefaultCollectionContext; + #collectionContext?: UmbMediaCollectionContext; constructor() { super(); @@ -24,23 +28,41 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { this.#collectionContext = collectionContext; this.#observeCollectionContext(); }); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('media') + .onSetup(() => { + return { data: { entityType: 'media', preset: {} } }; + }) + .onReject(() => { + this.#collectionContext?.requestCollection(); + }) + .onSubmit(() => { + this.#collectionContext?.requestCollection(); + }) + .observeRouteBuilder((routeBuilder) => { + this._editMediaPath = routeBuilder({}); + }); } #observeCollectionContext() { if (!this.#collectionContext) return; - this.observe(this.#collectionContext.items, (items) => (this._items = items), 'umbCollectionItemsObserver'); + this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading'); + + this.observe(this.#collectionContext.items, (items) => (this._items = items), '_observeItems'); this.observe( this.#collectionContext.selection.selection, (selection) => (this._selection = selection), - 'umbCollectionSelectionObserver', + '_observeSelection', ); } - #onOpen(item: UmbMediaCollectionItemModel) { - //TODO: Fix when we have dynamic routing - history.pushState(null, '', 'section/media/workspace/media/edit/' + item.unique); + #onOpen(event: Event, id: string) { + event.preventDefault(); + event.stopPropagation(); + window.history.pushState(null, '', this._editMediaPath + 'edit/' + id); } #onSelect(item: UmbMediaCollectionItemModel) { @@ -60,26 +82,37 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { } render() { - if (this._loading) { - return html`
`; - } - - if (this._items.length === 0) { - return html`

${this.localize.term('content_listViewNoItems')}

`; - } + return this._items.length === 0 ? this.#renderEmpty() : this.#renderItems(); + } + #renderEmpty() { + if (this._items.length > 0) return nothing; return html` -
- ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderCard(item), +
+ ${when( + this._loading, + () => html``, + () => html`

${this.localize.term('content_listViewNoItems')}

`, )}
`; } - #renderCard(item: UmbMediaCollectionItemModel) { + #renderItems() { + if (this._items.length === 0) return nothing; + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} +
+ ${when(this._loading, () => html``)} + `; + } + + #renderItem(item: UmbMediaCollectionItemModel) { // TODO: Fix the file extension when media items have a file extension. [?] return html` 0} ?selected=${this.#isSelected(item)} - @open=${() => this.#onOpen(item)} + @open=${(event: Event) => this.#onOpen(event, item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)} class="media-item" diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts index b1a10fb899..688b2a90c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts @@ -1,50 +1,32 @@ -import type { UmbMediaCollectionItemModel } from '../../../types.js'; -import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEditableMediaCollectionItemModel } from '../../../types.js'; +import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-media-table-column-name') export class UmbMediaTableColumnNameElement extends UmbLitElement implements UmbTableColumnLayoutElement { - @state() - private _editMediaPath = ''; - - @property({ type: Object, attribute: false }) column!: UmbTableColumn; - - @property({ type: Object, attribute: false }) item!: UmbTableItem; @property({ attribute: false }) - value!: UmbMediaCollectionItemModel; + value!: UmbEditableMediaCollectionItemModel; - constructor() { - super(); - - new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) - .addAdditionalPath('media') - .onSetup(() => { - return { data: { entityType: 'media', preset: {} } }; - }) - .observeRouteBuilder((routeBuilder) => { - this._editMediaPath = routeBuilder({}); - }); - } - - #onClick(event: Event) { - // TODO: [LK] Review the `stopPropagation` usage, as it causes a page reload. - // But we still need a say to prevent the `umb-table` from triggering a selection event. + #onClick(event: Event & { target: UUIButtonElement }) { + event.preventDefault(); event.stopPropagation(); + window.history.pushState(null, '', event.target.href); } render() { - return html``; + if (!this.value) return nothing; + return html` + + `; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts index a3a4cef9a6..9e42f79991 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts @@ -1,6 +1,6 @@ import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; @@ -14,6 +14,8 @@ import type { UmbTableOrderedEvent, UmbTableSelectedEvent, } from '@umbraco-cms/backoffice/components'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal'; import './column-layouts/media-table-column-name.element.js'; @@ -51,29 +53,52 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { @state() private _selection: Array = []; - @state() - private _skip: number = 0; - #collectionContext?: UmbDefaultCollectionContext; + #routeBuilder?: UmbModalRouteBuilder; + constructor() { super(); this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => { this.#collectionContext = collectionContext; - this.#observeCollectionContext(); }); + + this.#registerModalRoute(); + } + + #registerModalRoute() { + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath(':entityType') + .onSetup((params) => { + return { data: { entityType: params.entityType, preset: {} } }; + }) + .onReject(() => { + this.#collectionContext?.requestCollection(); + }) + .onSubmit(() => { + this.#collectionContext?.requestCollection(); + }) + .observeRouteBuilder((routeBuilder) => { + this.#routeBuilder = routeBuilder; + + // NOTE: Configuring the observations AFTER the route builder is ready, + // otherwise there is a race condition and `#collectionContext.items` tends to win. [LK] + this.#observeCollectionContext(); + }); } #observeCollectionContext() { if (!this.#collectionContext) return; + this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading'); + this.observe( this.#collectionContext.userDefinedProperties, (userDefinedProperties) => { this._userDefinedProperties = userDefinedProperties; this.#createTableHeadings(); }, - 'umbCollectionUserDefinedPropertiesObserver', + '_observeUserDefinedProperties', ); this.observe( @@ -82,7 +107,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { this._items = items; this.#createTableItems(this._items); }, - 'umbCollectionItemsObserver', + '_observeItems', ); this.observe( @@ -90,15 +115,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { (selection) => { this._selection = selection as string[]; }, - 'umbCollectionSelectionObserver', - ); - - this.observe( - this.#collectionContext.pagination.skip, - (skip) => { - this._skip = skip; - }, - 'umbCollectionSkipObserver', + '_observeSelection', ); } @@ -129,15 +146,20 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { const data = this._tableColumns?.map((column) => { + const editPath = this.#routeBuilder + ? this.#routeBuilder({ entityType: item.entityType }) + `edit/${item.unique}` + : ''; + return { columnAlias: column.alias, - value: column.elementName ? item : this.#getPropertyValueByAlias(item, column.alias), + value: column.elementName ? { item, editPath } : this.#getPropertyValueByAlias(item, column.alias), }; }) ?? []; return { id: item.unique, icon: item.icon, + entityType: 'media', data: data, }; }); @@ -145,6 +167,8 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { #getPropertyValueByAlias(item: UmbMediaCollectionItemModel, alias: string) { switch (alias) { + case 'contentTypeAlias': + return item.contentTypeAlias; case 'createDate': return item.createDate.toLocaleString(); case 'name': @@ -156,6 +180,8 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { return item.sortOrder; case 'updateDate': return item.updateDate.toLocaleString(); + case 'updater': + return item.updater; default: return item.values.find((value) => value.alias === alias)?.value ?? ''; } @@ -186,23 +212,34 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { } render() { - if (this._loading) { - return html`
`; - } + return this._tableItems.length === 0 ? this.#renderEmpty() : this.#renderItems(); + } - if (this._tableItems.length === 0) { - return html`

${this.localize.term('content_listViewNoItems')}

`; - } + #renderEmpty() { + if (this._tableItems.length > 0) return nothing; + return html` +
+ ${when( + this._loading, + () => html``, + () => html`

${this.localize.term('content_listViewNoItems')}

`, + )} +
+ `; + } + #renderItems() { + if (this._tableItems.length === 0) return nothing; return html` + @selected=${this.#handleSelect} + @deselected=${this.#handleDeselect} + @ordered=${this.#handleOrdering}> + ${when(this._loading, () => html``)} `; } From f4ae2637369ebce9f9a4921037806b13ec184a0b Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 1 May 2024 09:44:32 +0100 Subject: [PATCH 2/8] Replaced hardcoded edit path with constant helper Added stub file for media path constants helper. --- .../views/grid/media-grid-collection-view.element.ts | 7 +++++-- .../views/table/media-table-collection-view.element.ts | 4 +++- .../src/packages/media/media/index.ts | 1 + .../src/packages/media/media/paths.ts | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index da754c7398..43201b8839 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -1,3 +1,4 @@ +import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbMediaCollectionItemModel } from '../../types.js'; import type { UmbMediaCollectionContext } from '../../media-collection.context.js'; import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; @@ -59,10 +60,12 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { ); } - #onOpen(event: Event, id: string) { + #onOpen(event: Event, unique: string) { event.preventDefault(); event.stopPropagation(); - window.history.pushState(null, '', this._editMediaPath + 'edit/' + id); + + const url = this._editMediaPath + UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN.generateLocal({ unique }); + window.history.pushState(null, '', url); } #onSelect(item: UmbMediaCollectionItemModel) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts index 9e42f79991..86d2451247 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts @@ -1,3 +1,4 @@ +import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit'; @@ -147,7 +148,8 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { const data = this._tableColumns?.map((column) => { const editPath = this.#routeBuilder - ? this.#routeBuilder({ entityType: item.entityType }) + `edit/${item.unique}` + ? this.#routeBuilder({ entityType: item.entityType }) + + UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique }) : ''; return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 774113923c..dd00d3aa7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -5,6 +5,7 @@ export * from './workspace/index.js'; export * from './reference/index.js'; export * from './components/index.js'; export * from './entity.js'; +export * from './paths.js'; export * from './utils/index.js'; export { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts new file mode 100644 index 0000000000..d5eb6fdfd5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts @@ -0,0 +1,3 @@ +import { UmbPathPattern } from '@umbraco-cms/backoffice/router'; + +export const UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>('edit/:unique'); From c7d71c48dfebb38a1ce216f45091aae5383eaa86 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 1 May 2024 13:49:33 +0100 Subject: [PATCH 3/8] Collection: Syncs the Order By with Columns Displayed field --- .../config/order-by/order-by.element.ts | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts index 39ccfe8ba2..977830cf4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts @@ -1,34 +1,44 @@ -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbCollectionColumnConfiguration } from '../../../../core/collection/types.js'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; +import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; /** * @element umb-property-editor-ui-collection-order-by */ @customElement('umb-property-editor-ui-collection-order-by') export class UmbPropertyEditorUICollectionOrderByElement extends UmbLitElement implements UmbPropertyEditorUiElement { - private _value = ''; @property() - public set value(v: string) { - this._value = v; - this._options = this._options.map((option) => (option.value === v ? { ...option, selected: true } : option)); - } - public get value() { - return this._value; - } + public value: string = ''; + + public config?: UmbPropertyEditorConfigCollection; @state() - _options: Array