From 49330a45477144bdde4df2f0014b79cbaa6a4b47 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:36:09 +0200 Subject: [PATCH 001/134] Feature: Media Picker Modal --- .../collection/collection-view.manager.ts | 2 +- .../src/packages/media/media/index.ts | 1 + .../src/packages/media/media/manifests.ts | 2 + .../src/packages/media/media/modals/index.ts | 1 + .../packages/media/media/modals/manifests.ts | 3 + .../modals/media-picker/collection/index.ts | 1 + .../media-picker/collection/manifests.ts | 18 ++ ...media-picker-collection-toolbar.element.ts | 60 +++++ .../media-picker-collection.context.ts | 13 + .../media-picker-collection.element.ts | 46 ++++ .../modals/media-picker/collection/types.ts | 19 ++ ...dia-picker-grid-collection-view.element.ts | 135 ++++++++++ .../media-picker/collection/views/index.ts | 2 + .../collection/views/manifests.ts | 44 ++++ .../media-picker-table-column-name.element.ts | 65 +++++ ...ia-picker-table-collection-view.element.ts | 239 ++++++++++++++++++ .../media/media/modals/media-picker/index.ts | 3 + .../media/modals/media-picker/manifests.ts | 13 + .../media-picker-modal.element.ts | 33 +++ .../media-picker/media-picker-modal.token.ts | 19 ++ .../dashboard-examine-management.element.ts | 2 +- .../plugins/tiny-mce-mediapicker.plugin.ts | 9 +- 22 files changed, 726 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts index dd09955779..7fc6f58e72 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts @@ -8,7 +8,7 @@ import type { UmbRoute } from '@umbraco-cms/backoffice/router'; export interface UmbCollectionViewManagerConfig { defaultViewAlias?: string; - manifestFilter?: (manifest: ManifestCollectionView) => boolean + manifestFilter?: (manifest: ManifestCollectionView) => boolean; } export class UmbCollectionViewManager extends UmbControllerBase { 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..e793a193dd 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 @@ -10,5 +10,6 @@ export * from './utils/index.js'; export { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL } from './tree/index.js'; export { UMB_MEDIA_COLLECTION_ALIAS } from './collection/index.js'; export { UMB_MEDIA_MENU_ALIAS } from './menu/manifests.js'; +export { UMB_MEDIA_PICKER_MODAL } from './modals/media-picker/index.js'; export type { UmbMediaTreeItemModel } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index cbdd5167a5..78f43f1572 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -2,6 +2,7 @@ import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; +import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as propertyEditorsManifests } from './property-editors/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; @@ -14,6 +15,7 @@ export const manifests = [ ...entityActionsManifests, ...entityBulkActionsManifests, ...menuManifests, + ...modalManifests, ...propertyEditorsManifests, ...recycleBinManifests, ...repositoryManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts new file mode 100644 index 0000000000..6a64757856 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts @@ -0,0 +1 @@ +export * from './media-picker/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts new file mode 100644 index 0000000000..749db2bfc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as mediaPickerManifests } from './media-picker/manifests.js'; + +export const manifests = [...mediaPickerManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts new file mode 100644 index 0000000000..8a835e6afd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_PICKER_COLLECTION_ALIAS = 'Umb.Collection.MediaPicker'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts new file mode 100644 index 0000000000..060ad7fc0e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS } from '../../../collection/repository/index.js'; +import { manifests as collectionViewManifests } from './views/manifests.js'; +import { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; +import { UMB_MEDIA_PICKER_COLLECTION_ALIAS } from './index.js'; +import type { ManifestCollection } from '@umbraco-cms/backoffice/extension-registry'; + +const collectionManifest: ManifestCollection = { + type: 'collection', + alias: UMB_MEDIA_PICKER_COLLECTION_ALIAS, + name: 'Media Picker Collection', + api: UmbMediaPickerCollectionContext, + element: () => import('./media-picker-collection.element.js'), + meta: { + repositoryAlias: UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS, + }, +}; + +export const manifests = [collectionManifest, ...collectionViewManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts new file mode 100644 index 0000000000..d3b4e721fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts @@ -0,0 +1,60 @@ +import type { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; + +@customElement('umb-media-picker-collection-toolbar') +export class UmbMediaPickerCollectionToolbarElement extends UmbLitElement { + #collectionContext?: UmbMediaPickerCollectionContext; + + #inputTimer?: NodeJS.Timeout; + #inputTimerAmount = 500; + + constructor() { + super(); + + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance as UmbMediaPickerCollectionContext; + }); + } + + #updateSearch(event: InputEvent) { + const target = event.target as HTMLInputElement; + const filter = target.value || ''; + clearTimeout(this.#inputTimer); + this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount); + } + + render() { + return html` + + + `; + } + + static styles = [ + css` + :host { + height: 100%; + width: 100%; + display: flex; + justify-content: space-between; + white-space: nowrap; + gap: var(--uui-size-space-5); + align-items: center; + } + + #input-search { + width: 100%; + } + `, + ]; +} + +export default UmbMediaPickerCollectionToolbarElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-collection-toolbar': UmbMediaPickerCollectionToolbarElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts new file mode 100644 index 0000000000..8aa3368913 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts @@ -0,0 +1,13 @@ +import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from './types.js'; +import { UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js'; +import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMediaPickerCollectionContext extends UmbDefaultCollectionContext< + UmbMediaPickerCollectionItemModel, + UmbMediaPickerCollectionFilterModel +> { + constructor(host: UmbControllerHost) { + super(host, UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts new file mode 100644 index 0000000000..c695d54d48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts @@ -0,0 +1,46 @@ +import type { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; +import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_DEFAULT_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; +import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; + +import './media-picker-collection-toolbar.element.js'; + +@customElement('umb-media-picker-collection') +export class UmbMediaPickerCollectionElement extends UmbCollectionDefaultElement { + #mediaCollection?: UmbMediaPickerCollectionContext; + + @state() + private _progress = -1; + + constructor() { + super(); + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#mediaCollection = instance as UmbMediaPickerCollectionContext; + }); + } + + #onChange() { + this._progress = -1; + this.#mediaCollection?.requestCollection(); + } + + #onProgress(event: UmbProgressEvent) { + this._progress = event.progress; + } + + protected renderToolbar() { + return html` + + ${when(this._progress >= 0, () => html``)} + + `; + } +} + +export default UmbMediaPickerCollectionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-collection': UmbMediaPickerCollectionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts new file mode 100644 index 0000000000..8a1800cd0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts @@ -0,0 +1,19 @@ +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; + +export interface UmbMediaPickerCollectionFilterModel extends UmbCollectionFilterModel { + unique?: string; + dataTypeId?: string; + orderBy?: string; + orderDirection?: 'asc' | 'desc'; + userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>; +} + +export interface UmbMediaPickerCollectionItemModel { + unique: string; + createDate: Date; + creator?: string | null; + icon: string; + name: string; + updateDate: Date; + values: Array<{ alias: string; value: string }>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts new file mode 100644 index 0000000000..f6e721f643 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts @@ -0,0 +1,135 @@ +import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from '../../types.js'; +import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; + +@customElement('umb-media-picker-grid-collection-view') +export class UmbMediaPickerGridCollectionViewElement extends UmbLitElement { + @state() + private _items: Array = []; + + @state() + private _loading = false; + + @state() + private _selection: Array = []; + + #collectionContext?: UmbDefaultCollectionContext< + UmbMediaPickerCollectionItemModel, + UmbMediaPickerCollectionFilterModel + >; + + constructor() { + super(); + console.log('grid loaded'); + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (collectionContext) => { + this.#collectionContext = collectionContext; + this.#observeCollectionContext(); + }); + } + + #observeCollectionContext() { + if (!this.#collectionContext) return; + + this.observe(this.#collectionContext.items, (items) => (this._items = items), 'umbCollectionItemsObserver'); + + this.observe( + this.#collectionContext.selection.selection, + (selection) => (this._selection = selection), + 'umbCollectionSelectionObserver', + ); + } + + #onOpen(item: UmbMediaPickerCollectionItemModel) { + //TODO: Fix when we have dynamic routing + history.pushState(null, '', 'section/media/workspace/media/edit/' + item.unique); + } + + #onSelect(item: UmbMediaPickerCollectionItemModel) { + if (item.unique) { + this.#collectionContext?.selection.select(item.unique); + } + } + + #onDeselect(item: UmbMediaPickerCollectionItemModel) { + if (item.unique) { + this.#collectionContext?.selection.deselect(item.unique); + } + } + + #isSelected(item: UmbMediaPickerCollectionItemModel) { + return this.#collectionContext?.selection.isSelected(item.unique); + } + + render() { + if (this._loading) { + return html`
`; + } + + if (this._items.length === 0) { + return html`

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

`; + } + + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderCard(item), + )} +
+ `; + } + + #renderCard(item: UmbMediaPickerCollectionItemModel) { + // TODO: Fix the file extension when media items have a file extension. [?] + return html` + 0} + ?selected=${this.#isSelected(item)} + @open=${() => this.#onOpen(item)} + @selected=${() => this.#onSelect(item)} + @deselected=${() => this.#onDeselect(item)} + class="media-item" + file-ext="${item.icon}"> + + + + `; + } + + static styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + + .container { + display: flex; + justify-content: center; + align-items: center; + } + + #media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-rows: repeat(auto-fill, 200px); + gap: var(--uui-size-space-5); + } + `, + ]; +} + +export default UmbMediaPickerGridCollectionViewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-grid-collection-view': UmbMediaPickerGridCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts new file mode 100644 index 0000000000..d4d0999ad8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts @@ -0,0 +1,2 @@ +export const UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS = 'Umb.CollectionView.MediaPicker.Grid'; +export const UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS = 'Umb.CollectionView.MediaPicker.Table'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts new file mode 100644 index 0000000000..cd2c77ba41 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts @@ -0,0 +1,44 @@ +import { UMB_MEDIA_PICKER_COLLECTION_ALIAS } from '../index.js'; +import { UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS, UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS } from './index.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; +import type { ManifestCollectionView } from '@umbraco-cms/backoffice/extension-registry'; + +const gridViewManifest: ManifestCollectionView = { + type: 'collectionView', + alias: UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS, + name: 'Media Picker Grid Collection View', + element: () => import('./grid/media-picker-grid-collection-view.element.js'), + weight: 300, + meta: { + label: 'Grid', + icon: 'icon-grid', + pathName: 'grid', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_MEDIA_PICKER_COLLECTION_ALIAS, + }, + ], +}; + +const tableViewManifest: ManifestCollectionView = { + type: 'collectionView', + alias: UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS, + name: 'Media Picker Table Collection View', + element: () => import('./table/media-picker-table-collection-view.element.js'), + weight: 200, + meta: { + label: 'Table', + icon: 'icon-list', + pathName: 'table', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_MEDIA_PICKER_COLLECTION_ALIAS, + }, + ], +}; + +export const manifests = [gridViewManifest, tableViewManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts new file mode 100644 index 0000000000..6f8b0a5144 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts @@ -0,0 +1,65 @@ +import type { UmbMediaPickerCollectionItemModel } from '../../../types.js'; +import { css, customElement, html, property, state } 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'; + +@customElement('umb-media-picker-table-column-name') +export class UmbMediaPickerTableColumnNameElement 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!: UmbMediaPickerCollectionItemModel; + + 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. + event.stopPropagation(); + } + + render() { + return html``; + } + + static styles = [ + css` + uui-button { + text-align: left; + } + `, + ]; +} + +export default UmbMediaPickerTableColumnNameElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-table-column-name': UmbMediaPickerTableColumnNameElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts new file mode 100644 index 0000000000..22bcc051e3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts @@ -0,0 +1,239 @@ +import type { UmbCollectionColumnConfiguration } from '../../../../../../../core/collection/types.js'; +import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from '../../types.js'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import type { + UmbTableColumn, + UmbTableConfig, + UmbTableDeselectedEvent, + UmbTableElement, + UmbTableItem, + UmbTableOrderedEvent, + UmbTableSelectedEvent, +} from '@umbraco-cms/backoffice/components'; + +import './column-layouts/media-picker-table-column-name.element.js'; + +@customElement('umb-media-picker-table-collection-view') +export class UmbMediaPickerTableCollectionViewElement extends UmbLitElement { + @state() + private _loading = false; + + @state() + private _userDefinedProperties?: Array; + + @state() + private _items?: Array; + + @state() + private _tableConfig: UmbTableConfig = { + allowSelection: true, + }; + + @state() + private _tableColumns: Array = []; + + #systemColumns: Array = [ + { + name: this.localize.term('general_name'), + alias: 'entityName', + elementName: 'umb-media-table-column-name', + allowSorting: true, + }, + ]; + + @state() + private _tableItems: Array = []; + + @state() + private _selection: Array = []; + + @state() + private _skip: number = 0; + + #collectionContext?: UmbDefaultCollectionContext< + UmbMediaPickerCollectionItemModel, + UmbMediaPickerCollectionFilterModel + >; + + constructor() { + super(); + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (collectionContext) => { + this.#collectionContext = collectionContext; + this.#observeCollectionContext(); + }); + } + + #observeCollectionContext() { + if (!this.#collectionContext) return; + + this.observe( + this.#collectionContext.userDefinedProperties, + (userDefinedProperties) => { + this._userDefinedProperties = userDefinedProperties; + this.#createTableHeadings(); + }, + 'umbCollectionUserDefinedPropertiesObserver', + ); + + this.observe( + this.#collectionContext.items, + (items) => { + this._items = items; + this.#createTableItems(this._items); + }, + 'umbCollectionItemsObserver', + ); + + this.observe( + this.#collectionContext.selection.selection, + (selection) => { + this._selection = selection as string[]; + }, + 'umbCollectionSelectionObserver', + ); + + this.observe( + this.#collectionContext.pagination.skip, + (skip) => { + this._skip = skip; + }, + 'umbCollectionSkipObserver', + ); + } + + #createTableHeadings() { + if (this._userDefinedProperties && this._userDefinedProperties.length > 0) { + const userColumns: Array = this._userDefinedProperties.map((item) => { + return { + name: item.header, + alias: item.alias, + elementName: item.elementName, + allowSorting: true, + }; + }); + + this._tableColumns = [...this.#systemColumns, ...userColumns]; + } else { + this._tableColumns = [...this.#systemColumns]; + } + } + + #createTableItems(items: Array) { + if (this._tableColumns.length === 0) { + this.#createTableHeadings(); + } + + this._tableItems = items.map((item, rowIndex) => { + if (!item.unique) throw new Error('Item id is missing.'); + + const sortOrder = this._skip + rowIndex; + + const data = + this._tableColumns?.map((column) => { + return { + columnAlias: column.alias, + value: column.elementName ? item : this.#getPropertyValueByAlias(sortOrder, item, column.alias), + }; + }) ?? []; + + return { + id: item.unique, + icon: item.icon, + data: data, + }; + }); + } + + #getPropertyValueByAlias(sortOrder: number, item: UmbMediaPickerCollectionItemModel, alias: string) { + switch (alias) { + case 'createDate': + return item.createDate.toLocaleString(); + case 'entityName': + return item.name; + case 'owner': + return item.creator; + case 'sortOrder': + return sortOrder; + case 'updateDate': + return item.updateDate.toLocaleString(); + default: + return item.values.find((value) => value.alias === alias)?.value ?? ''; + } + } + + #handleSelect(event: UmbTableSelectedEvent) { + event.stopPropagation(); + const table = event.target as UmbTableElement; + const selection = table.selection; + this.#collectionContext?.selection.setSelection(selection); + } + + #handleDeselect(event: UmbTableDeselectedEvent) { + event.stopPropagation(); + const table = event.target as UmbTableElement; + const selection = table.selection; + this.#collectionContext?.selection.setSelection(selection); + } + + #handleOrdering(event: UmbTableOrderedEvent) { + const table = event.target as UmbTableElement; + const orderingColumn = table.orderingColumn; + const orderingDesc = table.orderingDesc; + this.#collectionContext?.setFilter({ + orderBy: orderingColumn, + orderDirection: orderingDesc ? 'desc' : 'asc', + }); + } + + render() { + if (this._loading) { + return html`
`; + } + + if (this._tableItems.length === 0) { + return html`

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

`; + } + + return html` + + `; + } + + static styles = [ + UmbTextStyles, + css` + :host { + display: block; + box-sizing: border-box; + height: auto; + width: 100%; + padding: var(--uui-size-space-3) var(--uui-size-space-6); + } + + .container { + display: flex; + justify-content: center; + align-items: center; + } + `, + ]; +} + +export default UmbMediaPickerTableCollectionViewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-table-collection-view': UmbMediaPickerTableCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts new file mode 100644 index 0000000000..ee381c8cdf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts @@ -0,0 +1,3 @@ +export * from './media-picker-modal.element.js'; +export * from './media-picker-modal.token.js'; +export * from './collection/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts new file mode 100644 index 0000000000..44a9cadc31 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts @@ -0,0 +1,13 @@ +import { manifests as collectionManifests } from './collection/manifests.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MediaPicker', + name: 'Media Picker Modal', + js: () => import('./media-picker-modal.element.js'), + }, +]; + +export const manifests = [...modals, ...collectionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts new file mode 100644 index 0000000000..95ee9d3073 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -0,0 +1,33 @@ +import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import './collection/views/grid/media-picker-grid-collection-view.element.js'; + +@customElement('umb-media-picker-modal') +export class UmbMediaPickerModalElement extends UmbModalBaseElement { + render() { + return html` + + +
+ + +
+
+ `; + } + + static styles = [css``]; +} + +export default UmbMediaPickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-modal': UmbMediaPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts new file mode 100644 index 0000000000..5fc7b4765b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMediaPickerModalData { + selection: string[]; +} + +export type UmbMediaPickerModalValue = { + selection: string[]; +}; + +export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( + 'Umb.Modal.MediaPicker', + { + modal: { + type: 'sidebar', + size: 'medium', + }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts b/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts index fe8c82b27f..5acb0cded7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts @@ -61,7 +61,7 @@ export class UmbDashboardExamineManagementElement extends UmbLitElement { - ` + ` : nothing; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 3451e0a5bf..b8dedf912d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -5,7 +5,12 @@ import type { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from '@umbraco-cms import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { sizeImageInEditor, uploadBlobImages, UMB_MEDIA_TREE_PICKER_MODAL } from '@umbraco-cms/backoffice/media'; +import { + sizeImageInEditor, + uploadBlobImages, + UMB_MEDIA_TREE_PICKER_MODAL, + UMB_MEDIA_PICKER_MODAL, +} from '@umbraco-cms/backoffice/media'; interface MediaPickerTargetData { altText?: string; @@ -137,7 +142,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { // TODO => startNodeId and startNodeIsVirtual do not exist on ContentTreeItemResponseModel - const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_TREE_PICKER_MODAL, { + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: false, hideTreeRoot: true, From 526dfebe56af40dbe715b6e3e7e0976ba04ae582 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:26:43 +0200 Subject: [PATCH 002/134] setup ui --- .../packages/media/media/collection/types.ts | 2 +- .../modals/media-picker/collection/index.ts | 1 - .../media-picker/collection/manifests.ts | 18 -- ...media-picker-collection-toolbar.element.ts | 60 ----- .../media-picker-collection.context.ts | 13 - .../media-picker-collection.element.ts | 46 ---- .../modals/media-picker/collection/types.ts | 19 -- ...dia-picker-grid-collection-view.element.ts | 135 ---------- .../media-picker/collection/views/index.ts | 2 - .../collection/views/manifests.ts | 44 ---- .../media-picker-table-column-name.element.ts | 65 ----- ...ia-picker-table-collection-view.element.ts | 239 ------------------ .../media/media/modals/media-picker/index.ts | 1 - .../media/modals/media-picker/manifests.ts | 3 +- .../media-picker-modal.element.ts | 176 ++++++++++++- .../media-picker/media-picker-modal.token.ts | 3 +- .../plugins/tiny-mce-mediapicker.plugin.ts | 9 +- .../src/packages/tiny-mce/utils.ts | 9 + 18 files changed, 187 insertions(+), 658 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts 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 9a5246af9f..23cdf0759d 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 @@ -5,7 +5,7 @@ export interface UmbMediaCollectionFilterModel extends UmbCollectionFilterModel dataTypeId?: string; orderBy?: string; orderDirection?: 'asc' | 'desc'; - userDefinedProperties: Array<{alias: string, header: string, isSystem: boolean}>; + userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>; } export interface UmbMediaCollectionItemModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts deleted file mode 100644 index 8a835e6afd..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const UMB_MEDIA_PICKER_COLLECTION_ALIAS = 'Umb.Collection.MediaPicker'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts deleted file mode 100644 index 060ad7fc0e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/manifests.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS } from '../../../collection/repository/index.js'; -import { manifests as collectionViewManifests } from './views/manifests.js'; -import { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; -import { UMB_MEDIA_PICKER_COLLECTION_ALIAS } from './index.js'; -import type { ManifestCollection } from '@umbraco-cms/backoffice/extension-registry'; - -const collectionManifest: ManifestCollection = { - type: 'collection', - alias: UMB_MEDIA_PICKER_COLLECTION_ALIAS, - name: 'Media Picker Collection', - api: UmbMediaPickerCollectionContext, - element: () => import('./media-picker-collection.element.js'), - meta: { - repositoryAlias: UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS, - }, -}; - -export const manifests = [collectionManifest, ...collectionViewManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts deleted file mode 100644 index d3b4e721fd..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection-toolbar.element.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; - -@customElement('umb-media-picker-collection-toolbar') -export class UmbMediaPickerCollectionToolbarElement extends UmbLitElement { - #collectionContext?: UmbMediaPickerCollectionContext; - - #inputTimer?: NodeJS.Timeout; - #inputTimerAmount = 500; - - constructor() { - super(); - - this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbMediaPickerCollectionContext; - }); - } - - #updateSearch(event: InputEvent) { - const target = event.target as HTMLInputElement; - const filter = target.value || ''; - clearTimeout(this.#inputTimer); - this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount); - } - - render() { - return html` - - - `; - } - - static styles = [ - css` - :host { - height: 100%; - width: 100%; - display: flex; - justify-content: space-between; - white-space: nowrap; - gap: var(--uui-size-space-5); - align-items: center; - } - - #input-search { - width: 100%; - } - `, - ]; -} - -export default UmbMediaPickerCollectionToolbarElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-collection-toolbar': UmbMediaPickerCollectionToolbarElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts deleted file mode 100644 index 8aa3368913..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from './types.js'; -import { UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js'; -import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbMediaPickerCollectionContext extends UmbDefaultCollectionContext< - UmbMediaPickerCollectionItemModel, - UmbMediaPickerCollectionFilterModel -> { - constructor(host: UmbControllerHost) { - super(host, UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts deleted file mode 100644 index c695d54d48..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/media-picker-collection.element.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { UmbMediaPickerCollectionContext } from './media-picker-collection.context.js'; -import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_DEFAULT_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; -import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; - -import './media-picker-collection-toolbar.element.js'; - -@customElement('umb-media-picker-collection') -export class UmbMediaPickerCollectionElement extends UmbCollectionDefaultElement { - #mediaCollection?: UmbMediaPickerCollectionContext; - - @state() - private _progress = -1; - - constructor() { - super(); - this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { - this.#mediaCollection = instance as UmbMediaPickerCollectionContext; - }); - } - - #onChange() { - this._progress = -1; - this.#mediaCollection?.requestCollection(); - } - - #onProgress(event: UmbProgressEvent) { - this._progress = event.progress; - } - - protected renderToolbar() { - return html` - - ${when(this._progress >= 0, () => html``)} - - `; - } -} - -export default UmbMediaPickerCollectionElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-collection': UmbMediaPickerCollectionElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts deleted file mode 100644 index 8a1800cd0a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; - -export interface UmbMediaPickerCollectionFilterModel extends UmbCollectionFilterModel { - unique?: string; - dataTypeId?: string; - orderBy?: string; - orderDirection?: 'asc' | 'desc'; - userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>; -} - -export interface UmbMediaPickerCollectionItemModel { - unique: string; - createDate: Date; - creator?: string | null; - icon: string; - name: string; - updateDate: Date; - values: Array<{ alias: string; value: string }>; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts deleted file mode 100644 index f6e721f643..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/grid/media-picker-grid-collection-view.element.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; - -@customElement('umb-media-picker-grid-collection-view') -export class UmbMediaPickerGridCollectionViewElement extends UmbLitElement { - @state() - private _items: Array = []; - - @state() - private _loading = false; - - @state() - private _selection: Array = []; - - #collectionContext?: UmbDefaultCollectionContext< - UmbMediaPickerCollectionItemModel, - UmbMediaPickerCollectionFilterModel - >; - - constructor() { - super(); - console.log('grid loaded'); - this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (collectionContext) => { - this.#collectionContext = collectionContext; - this.#observeCollectionContext(); - }); - } - - #observeCollectionContext() { - if (!this.#collectionContext) return; - - this.observe(this.#collectionContext.items, (items) => (this._items = items), 'umbCollectionItemsObserver'); - - this.observe( - this.#collectionContext.selection.selection, - (selection) => (this._selection = selection), - 'umbCollectionSelectionObserver', - ); - } - - #onOpen(item: UmbMediaPickerCollectionItemModel) { - //TODO: Fix when we have dynamic routing - history.pushState(null, '', 'section/media/workspace/media/edit/' + item.unique); - } - - #onSelect(item: UmbMediaPickerCollectionItemModel) { - if (item.unique) { - this.#collectionContext?.selection.select(item.unique); - } - } - - #onDeselect(item: UmbMediaPickerCollectionItemModel) { - if (item.unique) { - this.#collectionContext?.selection.deselect(item.unique); - } - } - - #isSelected(item: UmbMediaPickerCollectionItemModel) { - return this.#collectionContext?.selection.isSelected(item.unique); - } - - render() { - if (this._loading) { - return html`
`; - } - - if (this._items.length === 0) { - return html`

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

`; - } - - return html` -
- ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderCard(item), - )} -
- `; - } - - #renderCard(item: UmbMediaPickerCollectionItemModel) { - // TODO: Fix the file extension when media items have a file extension. [?] - return html` - 0} - ?selected=${this.#isSelected(item)} - @open=${() => this.#onOpen(item)} - @selected=${() => this.#onSelect(item)} - @deselected=${() => this.#onDeselect(item)} - class="media-item" - file-ext="${item.icon}"> - - - - `; - } - - static styles = [ - UmbTextStyles, - css` - :host { - display: flex; - flex-direction: column; - } - - .container { - display: flex; - justify-content: center; - align-items: center; - } - - #media-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - grid-template-rows: repeat(auto-fill, 200px); - gap: var(--uui-size-space-5); - } - `, - ]; -} - -export default UmbMediaPickerGridCollectionViewElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-grid-collection-view': UmbMediaPickerGridCollectionViewElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts deleted file mode 100644 index d4d0999ad8..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS = 'Umb.CollectionView.MediaPicker.Grid'; -export const UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS = 'Umb.CollectionView.MediaPicker.Table'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts deleted file mode 100644 index cd2c77ba41..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/manifests.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { UMB_MEDIA_PICKER_COLLECTION_ALIAS } from '../index.js'; -import { UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS, UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS } from './index.js'; -import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; -import type { ManifestCollectionView } from '@umbraco-cms/backoffice/extension-registry'; - -const gridViewManifest: ManifestCollectionView = { - type: 'collectionView', - alias: UMB_MEDIA_PICKER_GRID_COLLECTION_VIEW_ALIAS, - name: 'Media Picker Grid Collection View', - element: () => import('./grid/media-picker-grid-collection-view.element.js'), - weight: 300, - meta: { - label: 'Grid', - icon: 'icon-grid', - pathName: 'grid', - }, - conditions: [ - { - alias: UMB_COLLECTION_ALIAS_CONDITION, - match: UMB_MEDIA_PICKER_COLLECTION_ALIAS, - }, - ], -}; - -const tableViewManifest: ManifestCollectionView = { - type: 'collectionView', - alias: UMB_MEDIA_PICKER_TABLE_COLLECTION_VIEW_ALIAS, - name: 'Media Picker Table Collection View', - element: () => import('./table/media-picker-table-collection-view.element.js'), - weight: 200, - meta: { - label: 'Table', - icon: 'icon-list', - pathName: 'table', - }, - conditions: [ - { - alias: UMB_COLLECTION_ALIAS_CONDITION, - match: UMB_MEDIA_PICKER_COLLECTION_ALIAS, - }, - ], -}; - -export const manifests = [gridViewManifest, tableViewManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts deleted file mode 100644 index 6f8b0a5144..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/column-layouts/media-picker-table-column-name.element.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { UmbMediaPickerCollectionItemModel } from '../../../types.js'; -import { css, customElement, html, property, state } 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'; - -@customElement('umb-media-picker-table-column-name') -export class UmbMediaPickerTableColumnNameElement 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!: UmbMediaPickerCollectionItemModel; - - 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. - event.stopPropagation(); - } - - render() { - return html``; - } - - static styles = [ - css` - uui-button { - text-align: left; - } - `, - ]; -} - -export default UmbMediaPickerTableColumnNameElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-table-column-name': UmbMediaPickerTableColumnNameElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts deleted file mode 100644 index 22bcc051e3..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/collection/views/table/media-picker-table-collection-view.element.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { UmbCollectionColumnConfiguration } from '../../../../../../../core/collection/types.js'; -import type { UmbMediaPickerCollectionFilterModel, UmbMediaPickerCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; -import type { - UmbTableColumn, - UmbTableConfig, - UmbTableDeselectedEvent, - UmbTableElement, - UmbTableItem, - UmbTableOrderedEvent, - UmbTableSelectedEvent, -} from '@umbraco-cms/backoffice/components'; - -import './column-layouts/media-picker-table-column-name.element.js'; - -@customElement('umb-media-picker-table-collection-view') -export class UmbMediaPickerTableCollectionViewElement extends UmbLitElement { - @state() - private _loading = false; - - @state() - private _userDefinedProperties?: Array; - - @state() - private _items?: Array; - - @state() - private _tableConfig: UmbTableConfig = { - allowSelection: true, - }; - - @state() - private _tableColumns: Array = []; - - #systemColumns: Array = [ - { - name: this.localize.term('general_name'), - alias: 'entityName', - elementName: 'umb-media-table-column-name', - allowSorting: true, - }, - ]; - - @state() - private _tableItems: Array = []; - - @state() - private _selection: Array = []; - - @state() - private _skip: number = 0; - - #collectionContext?: UmbDefaultCollectionContext< - UmbMediaPickerCollectionItemModel, - UmbMediaPickerCollectionFilterModel - >; - - constructor() { - super(); - this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (collectionContext) => { - this.#collectionContext = collectionContext; - this.#observeCollectionContext(); - }); - } - - #observeCollectionContext() { - if (!this.#collectionContext) return; - - this.observe( - this.#collectionContext.userDefinedProperties, - (userDefinedProperties) => { - this._userDefinedProperties = userDefinedProperties; - this.#createTableHeadings(); - }, - 'umbCollectionUserDefinedPropertiesObserver', - ); - - this.observe( - this.#collectionContext.items, - (items) => { - this._items = items; - this.#createTableItems(this._items); - }, - 'umbCollectionItemsObserver', - ); - - this.observe( - this.#collectionContext.selection.selection, - (selection) => { - this._selection = selection as string[]; - }, - 'umbCollectionSelectionObserver', - ); - - this.observe( - this.#collectionContext.pagination.skip, - (skip) => { - this._skip = skip; - }, - 'umbCollectionSkipObserver', - ); - } - - #createTableHeadings() { - if (this._userDefinedProperties && this._userDefinedProperties.length > 0) { - const userColumns: Array = this._userDefinedProperties.map((item) => { - return { - name: item.header, - alias: item.alias, - elementName: item.elementName, - allowSorting: true, - }; - }); - - this._tableColumns = [...this.#systemColumns, ...userColumns]; - } else { - this._tableColumns = [...this.#systemColumns]; - } - } - - #createTableItems(items: Array) { - if (this._tableColumns.length === 0) { - this.#createTableHeadings(); - } - - this._tableItems = items.map((item, rowIndex) => { - if (!item.unique) throw new Error('Item id is missing.'); - - const sortOrder = this._skip + rowIndex; - - const data = - this._tableColumns?.map((column) => { - return { - columnAlias: column.alias, - value: column.elementName ? item : this.#getPropertyValueByAlias(sortOrder, item, column.alias), - }; - }) ?? []; - - return { - id: item.unique, - icon: item.icon, - data: data, - }; - }); - } - - #getPropertyValueByAlias(sortOrder: number, item: UmbMediaPickerCollectionItemModel, alias: string) { - switch (alias) { - case 'createDate': - return item.createDate.toLocaleString(); - case 'entityName': - return item.name; - case 'owner': - return item.creator; - case 'sortOrder': - return sortOrder; - case 'updateDate': - return item.updateDate.toLocaleString(); - default: - return item.values.find((value) => value.alias === alias)?.value ?? ''; - } - } - - #handleSelect(event: UmbTableSelectedEvent) { - event.stopPropagation(); - const table = event.target as UmbTableElement; - const selection = table.selection; - this.#collectionContext?.selection.setSelection(selection); - } - - #handleDeselect(event: UmbTableDeselectedEvent) { - event.stopPropagation(); - const table = event.target as UmbTableElement; - const selection = table.selection; - this.#collectionContext?.selection.setSelection(selection); - } - - #handleOrdering(event: UmbTableOrderedEvent) { - const table = event.target as UmbTableElement; - const orderingColumn = table.orderingColumn; - const orderingDesc = table.orderingDesc; - this.#collectionContext?.setFilter({ - orderBy: orderingColumn, - orderDirection: orderingDesc ? 'desc' : 'asc', - }); - } - - render() { - if (this._loading) { - return html`
`; - } - - if (this._tableItems.length === 0) { - return html`

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

`; - } - - return html` - - `; - } - - static styles = [ - UmbTextStyles, - css` - :host { - display: block; - box-sizing: border-box; - height: auto; - width: 100%; - padding: var(--uui-size-space-3) var(--uui-size-space-6); - } - - .container { - display: flex; - justify-content: center; - align-items: center; - } - `, - ]; -} - -export default UmbMediaPickerTableCollectionViewElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-table-collection-view': UmbMediaPickerTableCollectionViewElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts index ee381c8cdf..0c0aa9d430 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts @@ -1,3 +1,2 @@ export * from './media-picker-modal.element.js'; export * from './media-picker-modal.token.js'; -export * from './collection/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts index 44a9cadc31..14a43cb9f7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts @@ -1,4 +1,3 @@ -import { manifests as collectionManifests } from './collection/manifests.js'; import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; const modals: Array = [ @@ -10,4 +9,4 @@ const modals: Array = [ }, ]; -export const manifests = [...modals, ...collectionManifests]; +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 95ee9d3073..1868ff7b0a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,14 +1,71 @@ +import { UmbMediaCollectionRepository } from '../../collection/repository/media-collection.repository.js'; +import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../collection/types.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import './collection/views/grid/media-picker-grid-collection-view.element.js'; +import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbId } from '@umbraco-cms/backoffice/id'; @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { + #collectionRepository = new UmbMediaCollectionRepository(this); + + @state() + private _items: Array = []; + + @state() + private _selection: Array = []; + + @state() + private _currentPath = 'media-root'; + + @state() + private _typingNewFolder = false; + + @state() + private _paths: Array<{ name: string; unique: string }> = [{ name: 'Media', unique: 'media-root' }]; + + connectedCallback(): void { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this.#getCollection(); + } + + async #getCollection() { + const params: UmbMediaCollectionFilterModel = { + // TODO whatever this is for... + userDefinedProperties: [{ alias: 'type', header: '', isSystem: true }], + }; + + const { data } = await this.#collectionRepository.requestCollection(params); + this._items = data?.items ?? []; + + console.log(data?.items); + } + + #focusFolderInput() { + this._typingNewFolder = true; + requestAnimationFrame(() => { + const element = this.getHostElement().shadowRoot!.querySelector('#new-folder') as UUIInputElement; + element.focus(); + element.select(); + }); + } + + #addFolder(e: UUIInputEvent) { + const name = e.target.value as string; + if (name) { + const unique = UmbId.new(); + this._paths = [...this._paths, { name, unique }]; + this._currentPath = unique; + } + this._typingNewFolder = false; + } + render() { return html` - - + + ${this.#renderBody()}

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

` + : html`
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderCard(item), + )} +
` + } + `; + } + + #renderToolbar() { + return html`
+ + +
+ ${this.#renderPath()}`; + } + + #goToFolder(unique: string) { + this._paths = [...this._paths].slice(0, this._paths.findIndex((path) => path.unique === unique) + 1); + this._currentPath = unique; + } + + #renderPath() { + return html`
+ ${repeat( + this._paths, + (path) => path.unique, + (path) => + html` this.#goToFolder(path.unique)}>/`, + )}${this._typingNewFolder + ? html`` + : html`+`} +
`; + } + + #renderCard(item: UmbMediaCollectionItemModel) { + return html` + 0} + file-ext=${item.values.find((value) => value.alias === 'type')?.value ?? item.icon}> + + `; + } + + static styles = [ + css` + #toolbar { + display: flex; + gap: var(--uui-size-6); + align-items: flex-start; + } + #search { + flex: 1; + } + #search uui-input { + width: 100%; + margin-bottom: var(--uui-size-3); + } + #search uui-icon { + height: 100%; + display: flex; + align-items: stretch; + padding-left: var(--uui-size-3); + } + #media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-rows: repeat(auto-fill, 200px); + gap: var(--uui-size-space-5); + } + + #path { + display: flex; + align-items: center; + margin-bottom: var(--uui-size-3); + } + #path uui-button { + font-weight: bold; + } + #path uui-input { + height: 100%; + } + `, + ]; } export default UmbMediaPickerModalElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts index 5fc7b4765b..d842557104 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -1,7 +1,8 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMediaPickerModalData { - selection: string[]; + selection?: string[]; + multiple?: boolean; } export type UmbMediaPickerModalValue = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index b8dedf912d..62ce0101b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -1,3 +1,4 @@ +import { getGuid } from '../utils.js'; import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL } from '../modals/media-caption-alt-text/media-caption-alt-text-modal.token.js'; import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '../components/input-tiny-mce/tiny-mce-plugin.js'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; @@ -145,14 +146,10 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: false, - hideTreeRoot: true, - - //startNodeId, + selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], + //hideTreeRoot: true, //startNodeIsVirtual, }, - value: { - selection: currentTarget.udi ? [...currentTarget.udi] : [], - }, }); if (!modalHandler) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts new file mode 100644 index 0000000000..3078f5cf6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts @@ -0,0 +1,9 @@ +export function getGuid(udi: string) { + if (!udi.startsWith('umb://')) throw new Error('udi does not start with umb://'); + + const withoutScheme = udi.replace('umb://', ''); + const withoutHost = withoutScheme.split('/')[1]; + if (withoutHost.length !== 32) throw new Error('udi is not 32 chars'); + + return `${withoutHost.substring(0, 8)}-${withoutHost.substring(8, 12)}-${withoutHost.substring(12, 16)}-${withoutHost.substring(16, 20)}-${withoutHost.substring(20)}`; +} From 73daaa2df01ee7f893f2720d0b362131f190e514 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:00:48 +0200 Subject: [PATCH 003/134] media picker show items --- .../packages/media/media-types/utils/index.ts | 16 ++- .../dropzone-media/dropzone-media.element.ts | 35 +++-- .../media-picker-modal.element.ts | 136 ++++++++++++++---- .../media-picker/media-picker-modal.token.ts | 4 +- .../plugins/tiny-mce-mediapicker.plugin.ts | 6 +- 5 files changed, 147 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts index b67055a38e..a4c4fce190 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts @@ -1,13 +1,16 @@ +// They are currently hardcoded on the backend. Go by GUIDs in case they get renamed..? export enum UmbMediaTypeFileType { - SVG = 'Vector Graphics (SVG)', - IMAGE = 'Image', - AUDIO = 'Audio', - VIDEO = 'Video', - ARTICLE = 'Article', - FILE = 'File', + SVG = 'c4b1efcf-a9d5-41c4-9621-e9d273b52a9c', //Vector Graphics (SVG) + IMAGE = 'cc07b313-0843-4aa8-bbda-871c8da728c8', //Image + AUDIO = 'a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3', //Audio + VIDEO = 'f6c515bb-653c-4bdc-821c-987729ebe327', //Video + ARTICLE = 'a43e3414-9599-4230-a7d3-943a21b20122', //Article + FILE = '4c52d8ab-54e6-40cd-999c-7a5f24903e4d', //File + FOLDER = 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d', //Folder } export function getMediaTypeByFileExtension(extension: string) { + if (!extension) return UmbMediaTypeFileType.FOLDER; if (extension === 'svg') return UmbMediaTypeFileType.SVG; if (['jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff', 'tif', 'webp'].includes(extension)) return UmbMediaTypeFileType.IMAGE; @@ -18,6 +21,7 @@ export function getMediaTypeByFileExtension(extension: string) { } export function getMediaTypeByFileMimeType(mimetype: string) { + if (!mimetype) return UmbMediaTypeFileType.FOLDER; if (mimetype === 'image/svg+xml') return UmbMediaTypeFileType.SVG; const [type, extension] = mimetype.split('/'); if (type === 'image') return UmbMediaTypeFileType.IMAGE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts index 6a03dc5e19..f606801cea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -68,18 +68,26 @@ export class UmbDropzoneMediaElement extends UmbLitElement { } #getMediaTypeFromMime(mimetype: string): UmbAllowedMediaTypeModel { - const mediaTypeName = getMediaTypeByFileMimeType(mimetype); - return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; + // TODO: We need to make sure we get the folder media type. Find a better way. The unique is currently hardcoded on the backend f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d + const mediaType = getMediaTypeByFileMimeType(mimetype); + return this.#allowedMediaTypes.find((type) => type.unique === mediaType)!; } async #uploadHandler(files: Array) { - const queue = files.map((file): UmbTemporaryFileQueueModel => ({ file })); + //TODO: Folders uploaded via UUIDropzone are always empty. Investigate why. + + const folders = files.filter((item) => !item.type).map((file): UmbTemporaryFileQueueModel => ({ file })); + const mediaItems = files.filter((item) => item.type); + + const queue = mediaItems.map((file): UmbTemporaryFileQueueModel => ({ file })); + const uploaded = await this.#fileManager.upload(queue); - return uploaded; + return [...folders, ...uploaded]; } async #onFileUpload(event: UUIFileDropzoneEvent) { const files: Array = event.detail.files; + if (!files.length) return; const uploads = await this.#uploadHandler(files); @@ -100,19 +108,22 @@ export class UmbDropzoneMediaElement extends UmbLitElement { updateDate: null, }, ], - values: [ - { - alias: 'umbracoFile', - value: { src: upload.unique }, - culture: null, - segment: null, - }, - ], + values: upload.file.type + ? [ + { + alias: 'umbracoFile', + value: { src: upload.unique }, + culture: null, + segment: null, + }, + ] + : [], }; const { data } = await this.#mediaDetailRepository.createScaffold(preset); if (!data) return; + // TODO Get the parent... await this.#mediaDetailRepository.create(data, null); this.dispatchEvent(new UmbChangeEvent()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 1868ff7b0a..214938b9d7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,46 +1,86 @@ -import { UmbMediaCollectionRepository } from '../../collection/repository/media-collection.repository.js'; -import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../collection/types.js'; +import type { UmbMediaDetailModel } from '../../types.js'; +import { UmbMediaDetailRepository } from '../../repository/index.js'; +import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; +import type { UmbMediaTreeItemModel } from '../../tree/types.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbMediaTypeFileType } from '@umbraco-cms/backoffice/media-type'; + +// Folder media type unique from backend +const FOLDER = 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d'; + +interface MediaPath { + name: string; + unique: string | null; +} @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { - #collectionRepository = new UmbMediaCollectionRepository(this); + #mediaTreeRepository = new UmbMediaTreeRepository(this); + #mediaDetailRepository = new UmbMediaDetailRepository(this); @state() - private _items: Array = []; + private _items: Array = []; @state() - private _selection: Array = []; + private _selectableFolders = false; @state() - private _currentPath = 'media-root'; + private _paths: Array = [{ name: 'Media', unique: null }]; + + @state() + private _currentPath: string | null = null; @state() private _typingNewFolder = false; - @state() - private _paths: Array<{ name: string; unique: string }> = [{ name: 'Media', unique: 'media-root' }]; - connectedCallback(): void { super.connectedCallback(); - this._selection = this.data?.selection ?? []; - this.#getCollection(); + this._selectableFolders = this.data?.selectableFolders ?? false; + this._currentPath = this.data?.startNode ?? null; + this.#loadPath(); } - async #getCollection() { - const params: UmbMediaCollectionFilterModel = { - // TODO whatever this is for... - userDefinedProperties: [{ alias: 'type', header: '', isSystem: true }], - }; + async #loadPath() { + if (this._currentPath) { + const { data } = await this.#mediaTreeRepository.requestTreeItemAncestors({ + descendantUnique: this._currentPath, + }); + const paths = data?.map((item) => ({ name: item.name, unique: item.unique })) ?? []; + this._paths = [...this._paths, ...paths]; + } - const { data } = await this.#collectionRepository.requestCollection(params); - this._items = data?.items ?? []; + this.#loadMediaFolder(); + } - console.log(data?.items); + async #loadMediaFolder() { + if (this._currentPath) { + const { data } = await this.#mediaTreeRepository.requestTreeItemsOf({ + parentUnique: this._currentPath, + skip: 0, + take: 100, + }); + this._items = data?.items ?? []; + } else { + const { data } = await this.#mediaTreeRepository.requestRootTreeItems({ skip: 0, take: 100 }); + this._items = data?.items ?? []; + } + } + + #goToFolder(unique: string | null) { + this._paths = [...this._paths].slice(0, this._paths.findIndex((path) => path.unique === unique) + 1); + this._currentPath = unique; + this.#loadMediaFolder(); + } + + #onFolderOpen(item: UmbMediaTreeItemModel) { + if (item.mediaType.unique !== FOLDER) return; + this._paths = [...this._paths, { name: item.name, unique: item.unique }]; + this._currentPath = item.unique; + this.#loadMediaFolder(); } #focusFolderInput() { @@ -52,14 +92,51 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement = { + unique, + mediaType: { + unique: UmbMediaTypeFileType.FOLDER, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: name, + createDate: null, + updateDate: null, + }, + ], + }; + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + if (data) { + await this.#mediaDetailRepository.create(data, parentUnique); + } } this._typingNewFolder = false; + this.#loadMediaFolder(); + } + + #onSelected(item: UmbMediaTreeItemModel) { + const selection = this.data?.multiple ? [...this.value.selection, item.unique] : [item.unique]; + this.modalContext?.setValue({ selection }); + if (this.data?.submitOnSelection) { + this._submitModal(); + } + } + + #onDeselected(item: UmbMediaTreeItemModel) { + const selection = this.value.selection.filter((value) => value !== item.unique); + this.modalContext?.setValue({ selection }); } render() { @@ -99,7 +176,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement + placeholder=${this.localize.term('placeholders_search') + '(TODO)'}> @@ -109,11 +186,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement path.unique === unique) + 1); - this._currentPath = unique; - } - #renderPath() { return html`
${repeat( @@ -137,13 +209,17 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement`; } - #renderCard(item: UmbMediaCollectionItemModel) { + #renderCard(item: UmbMediaTreeItemModel) { return html` 0} - file-ext=${item.values.find((value) => value.alias === 'type')?.value ?? item.icon}> + @open=${() => this.#onFolderOpen(item)} + @selected=${() => this.#onSelected(item)} + @deselected=${() => this.#onDeselected(item)} + ?selected=${this.value?.selection?.find((value) => value === item.unique)} + ?selectable=${item.mediaType.unique !== FOLDER || this._selectableFolders} + ?select-only=${item.mediaType.unique !== FOLDER} + file-ext=${item.mediaType.unique !== FOLDER ? item.mediaType.icon : ''}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts index d842557104..6f404d0fab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -1,7 +1,9 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMediaPickerModalData { - selection?: string[]; + startNode?: string | null; + selectableFolders?: boolean; + submitOnSelection?: boolean; multiple?: boolean; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 62ce0101b2..c01027333a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -146,10 +146,14 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: false, - selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], + //selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], + submitOnSelection: true, //hideTreeRoot: true, //startNodeIsVirtual, }, + value: { + selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], + }, }); if (!modalHandler) return; From e9790a082a338decd845c61c3243b471fb969792 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:26:07 +0200 Subject: [PATCH 004/134] dropzone handler --- .../dropzone-media/dropzone-media.element.ts | 15 +++++++++++---- .../media-picker/media-picker-modal.element.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts index f606801cea..2cd4fc23ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -1,7 +1,7 @@ import { UmbMediaDetailRepository } from '../../repository/index.js'; import type { UmbMediaDetailModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { type UmbAllowedMediaTypeModel, @@ -25,6 +25,14 @@ export class UmbDropzoneMediaElement extends UmbLitElement { @state() private queue: Array = []; + @property({ attribute: false }) + parentUnique: string | null = null; + + public getDropzoneElement() { + const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; + return element; + } + constructor() { super(); @@ -123,8 +131,7 @@ export class UmbDropzoneMediaElement extends UmbLitElement { const { data } = await this.#mediaDetailRepository.createScaffold(preset); if (!data) return; - // TODO Get the parent... - await this.#mediaDetailRepository.create(data, null); + await this.#mediaDetailRepository.create(data, this.parentUnique); this.dispatchEvent(new UmbChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 214938b9d7..9f10fd7be0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -2,6 +2,7 @@ import type { UmbMediaDetailModel } from '../../types.js'; import { UmbMediaDetailRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import type { UmbMediaTreeItemModel } from '../../tree/types.js'; +import type { UmbDropzoneMediaElement } from '../../components/index.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; @@ -139,6 +140,11 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement @@ -157,6 +163,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#loadMediaFolder()} .parentUnique=${this._currentPath}> ${ !this._items.length ? html`

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

` @@ -181,7 +188,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement
- + ${this.#renderPath()}`; } From 2bfd14c4fedb9ce49614202189387c131014d82d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:30:06 +0200 Subject: [PATCH 005/134] browse --- .../dropzone-media/dropzone-media.element.ts | 4 ++-- .../media-picker/media-picker-modal.element.ts | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts index 2cd4fc23ea..6f999a43a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -28,9 +28,9 @@ export class UmbDropzoneMediaElement extends UmbLitElement { @property({ attribute: false }) parentUnique: string | null = null; - public getDropzoneElement() { + public browse() { const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; - return element; + return element.browse(); } constructor() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 9f10fd7be0..3ed4c59bbf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -5,7 +5,7 @@ import type { UmbMediaTreeItemModel } from '../../tree/types.js'; import type { UmbDropzoneMediaElement } from '../../components/index.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat, query } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbMediaTypeFileType } from '@umbraco-cms/backoffice/media-type'; @@ -38,6 +38,9 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement @@ -191,7 +189,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement + @click=${() => this._dropzone?.browse()}> ${this.#renderPath()}`; } From 05e4da6d5ee1753064f4833d4904df8edc2a045a Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:53:20 +0200 Subject: [PATCH 006/134] comment out --- .../packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index c01027333a..d566876ce6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -146,9 +146,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: false, - //selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], submitOnSelection: true, - //hideTreeRoot: true, //startNodeIsVirtual, }, value: { From f0238c51aeaa653ceeaa972102da142ec48a4951 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:58:21 +0200 Subject: [PATCH 007/134] url repo --- .../packages/media/media/repository/index.ts | 1 + .../media/media/repository/manifests.ts | 3 +- .../media/media/repository/url/index.ts | 2 ++ .../media/media/repository/url/manifests.ts | 13 +++++++ .../repository/url/media-url.repository.ts | 25 ++++++++++++++ .../url/media-url.server.data-source.ts | 34 +++++++++++++++++++ 6 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts index 480a724215..30dfbb63e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts @@ -1,4 +1,5 @@ export { UmbMediaDetailRepository, UMB_MEDIA_DETAIL_REPOSITORY_ALIAS } from './detail/index.js'; export { UmbMediaItemRepository, UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from './item/index.js'; +export { UmbMediaUrlRepository, UMB_MEDIA_URL_REPOSITORY_ALIAS } from './url/index.js'; export type { UmbMediaItemModel } from './item/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts index bb35952020..66aa3adfa7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts @@ -1,4 +1,5 @@ import { manifests as detailManifests } from './detail/manifests.js'; import { manifests as itemManifests } from './item/manifests.js'; +import { manifests as urlManifests } from './url/manifests.js'; -export const manifests = [...detailManifests, ...itemManifests]; +export const manifests = [...detailManifests, ...itemManifests, ...urlManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts new file mode 100644 index 0000000000..1d3ef9c518 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts @@ -0,0 +1,2 @@ +export { UmbMediaUrlRepository } from './media-url.repository.js'; +export { UMB_MEDIA_URL_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts new file mode 100644 index 0000000000..5d6e07225d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts @@ -0,0 +1,13 @@ +import { UmbMediaUrlRepository } from './media-url.repository.js'; +import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_MEDIA_URL_REPOSITORY_ALIAS = 'Umb.Repository.Media.Url'; + +const urlRepository: ManifestRepository = { + type: 'repository', + alias: UMB_MEDIA_URL_REPOSITORY_ALIAS, + name: 'Media Url Repository', + api: UmbMediaUrlRepository, +}; + +export const manifests = [urlRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts new file mode 100644 index 0000000000..cb4b913b89 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts @@ -0,0 +1,25 @@ +import { UmbMediaUrlServerDataSource } from './media-url.server.data-source.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbMediaUrlRepository extends UmbRepositoryBase { + #urlDataSource: UmbMediaUrlServerDataSource; + + constructor(host: UmbControllerHost) { + super(host); + this.#urlDataSource = new UmbMediaUrlServerDataSource(this); + } + + /** + * Read the urls of the media items + * @param {string} id + * @param {Array} variantIds + * @return {*} + * @memberof UmbMediaUrlRepository + */ + async readUrls(uniques: Array) { + if (!uniques) throw new Error('uniques are missing'); + const { data } = await this.#urlDataSource.getUrls(uniques); + return { data }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts new file mode 100644 index 0000000000..c192978efb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts @@ -0,0 +1,34 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A server data source for Media Urls + * @export + * @class UmbMediaUrlServerDataSource + * @implements {DocumentTreeDataSource} + */ +export class UmbMediaUrlServerDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbMediaUrlServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbMediaUrlServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Publish one or more variants of a Document + * @param {string} unique + * @param {Array} variantIds + * @return {*} + * @memberof UmbMediaUrlServerDataSource + */ + async getUrls(uniques: Array) { + if (!uniques) throw new Error('Id is missing'); + return tryExecuteAndNotify(this.#host, MediaService.getMediaUrls({ id: uniques })); + } +} From 06e2c365a2e82ba58a4a20756e884f206f8b2fce Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:10:54 +0200 Subject: [PATCH 008/134] media url repository and item --- .../repository/item/media-item.repository.ts | 7 +++ .../item/media-item.server.data-source.ts | 11 +++++ .../media/media/repository/url/manifests.ts | 12 ++++- .../repository/url/media-url.repository.ts | 26 +++------- .../url/media-url.server.data-source.ts | 47 ++++++++++++------- .../media/repository/url/media-url.store.ts | 26 ++++++++++ .../media/media/repository/url/types.ts | 12 +++++ 7 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts index be841f4023..43c46ccd5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts @@ -5,8 +5,15 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository'; export class UmbMediaItemRepository extends UmbItemRepositoryBase { + #dataSource: UmbMediaItemServerDataSource; + constructor(host: UmbControllerHost) { super(host, UmbMediaItemServerDataSource, UMB_MEDIA_ITEM_STORE_CONTEXT); + this.#dataSource = new UmbMediaItemServerDataSource(this); + } + + async search({ query, skip, take }: { query: string; skip: number; take: number }) { + return this.#dataSource.search({ query, skip, take }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts index a1ffb549c7..8d9331df93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts @@ -3,6 +3,7 @@ import type { MediaItemResponseModel } from '@umbraco-cms/backoffice/external/ba import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for Media items that fetches data from the server @@ -14,6 +15,7 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< MediaItemResponseModel, UmbMediaItemModel > { + #host: UmbControllerHost; /** * Creates an instance of UmbMediaItemServerDataSource. * @param {UmbControllerHost} host @@ -24,6 +26,15 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< getItems, mapper, }); + this.#host = host; + } + async search({ query, skip, take }: { query: string; skip: number; take: number }) { + const { data, error } = await tryExecuteAndNotify( + this.#host, + MediaService.getItemMediaSearch({ query, skip, take }), + ); + const mapped = data?.items.map((item) => mapper(item)); + return { data: mapped, error }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts index 5d6e07225d..c60e50c095 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts @@ -1,7 +1,8 @@ import { UmbMediaUrlRepository } from './media-url.repository.js'; -import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestItemStore, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_MEDIA_URL_REPOSITORY_ALIAS = 'Umb.Repository.Media.Url'; +export const UMB_MEDIA_URL_STORE_ALIAS = 'Umb.Store.MediaUrl'; const urlRepository: ManifestRepository = { type: 'repository', @@ -10,4 +11,11 @@ const urlRepository: ManifestRepository = { api: UmbMediaUrlRepository, }; -export const manifests = [urlRepository]; +const urlStore: ManifestItemStore = { + type: 'itemStore', + alias: UMB_MEDIA_URL_STORE_ALIAS, + name: 'Media Url Store', + api: () => import('./media-url.store.js'), +}; + +export const manifests = [urlRepository, urlStore]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts index cb4b913b89..7e3a58f5b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts @@ -1,25 +1,13 @@ +import type { UmbMediaUrlModel } from './types.js'; +import { UMB_MEDIA_URL_STORE_CONTEXT } from './media-url.store.js'; import { UmbMediaUrlServerDataSource } from './media-url.server.data-source.js'; +import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; - -export class UmbMediaUrlRepository extends UmbRepositoryBase { - #urlDataSource: UmbMediaUrlServerDataSource; +export class UmbMediaUrlRepository extends UmbItemRepositoryBase { constructor(host: UmbControllerHost) { - super(host); - this.#urlDataSource = new UmbMediaUrlServerDataSource(this); - } - - /** - * Read the urls of the media items - * @param {string} id - * @param {Array} variantIds - * @return {*} - * @memberof UmbMediaUrlRepository - */ - async readUrls(uniques: Array) { - if (!uniques) throw new Error('uniques are missing'); - const { data } = await this.#urlDataSource.getUrls(uniques); - return { data }; + super(host, UmbMediaUrlServerDataSource, UMB_MEDIA_URL_STORE_CONTEXT); } } + +export default UmbMediaUrlRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts index c192978efb..7a424240ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts @@ -1,6 +1,7 @@ +import type { UmbMediaUrlModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { MediaService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; /** * A server data source for Media Urls @@ -8,27 +9,37 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbMediaUrlServerDataSource * @implements {DocumentTreeDataSource} */ -export class UmbMediaUrlServerDataSource { - #host: UmbControllerHost; - +export class UmbMediaUrlServerDataSource extends UmbItemServerDataSourceBase< + MediaUrlInfoResponseModel, + UmbMediaUrlModel +> { /** * Creates an instance of UmbMediaUrlServerDataSource. * @param {UmbControllerHost} host * @memberof UmbMediaUrlServerDataSource */ constructor(host: UmbControllerHost) { - this.#host = host; - } - - /** - * Publish one or more variants of a Document - * @param {string} unique - * @param {Array} variantIds - * @return {*} - * @memberof UmbMediaUrlServerDataSource - */ - async getUrls(uniques: Array) { - if (!uniques) throw new Error('Id is missing'); - return tryExecuteAndNotify(this.#host, MediaService.getMediaUrls({ id: uniques })); + super(host, { + getItems, + mapper, + }); } } + +/* eslint-disable local-rules/no-direct-api-import */ +const getItems = (uniques: Array) => MediaService.getMediaUrls({ id: uniques }); + +const mapper = (item: MediaUrlInfoResponseModel): UmbMediaUrlModel => { + const url = item.urlInfos.length ? item.urlInfos[0].url : undefined; + const extension = url ? url.slice(url.lastIndexOf('.') + 1, url.length) : undefined; + + return { + unique: item.id, + url, + extension, + /*info: item.urlInfos.map((urlInfo) => ({ + ...urlInfo, + extension: '', + })),*/ + }; +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts new file mode 100644 index 0000000000..26f874b757 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts @@ -0,0 +1,26 @@ +import type { UmbMediaDetailModel } from '../../types.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemStoreBase } from '@umbraco-cms/backoffice/store'; + +/** + * @export + * @class UmbMediaUrlStore + * @extends {UmbStoreBase} + * @description - Data Store for Media urls + */ + +export class UmbMediaUrlStore extends UmbItemStoreBase { + /** + * Creates an instance of UmbMediaUrlStore. + * @param {UmbControllerHost} host + * @memberof UmbMediaUrlStore + */ + constructor(host: UmbControllerHost) { + super(host, UMB_MEDIA_URL_STORE_CONTEXT.toString()); + } +} + +export default UmbMediaUrlStore; + +export const UMB_MEDIA_URL_STORE_CONTEXT = new UmbContextToken('UmbMediaUrlStore'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts new file mode 100644 index 0000000000..b582e7685f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts @@ -0,0 +1,12 @@ +export interface UmbMediaUrlModel { + unique: string; + url?: string; + extension?: string; + //info?: Array; +} +/* +export interface UmbMediaUrlInfoModel { + culture?: string | null; + url: string; + extension?: string; +}*/ From 862b27acb6c12bf5cb7b0d0e63e3f225a7c6f6cc Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:11:02 +0200 Subject: [PATCH 009/134] media picker changes --- .../packages/media/media-types/utils/index.ts | 10 + .../modals/media-picker/components/index.ts | 1 + .../media-picker-folder-path.element.ts | 164 +++++++++++ .../media/media/modals/media-picker/index.ts | 1 + .../media-picker-modal.element.ts | 254 ++++++++---------- .../media-picker/media-picker-modal.token.ts | 2 +- .../plugins/tiny-mce-mediapicker.plugin.ts | 7 +- 7 files changed, 293 insertions(+), 146 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts index a4c4fce190..e5f3287c90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts @@ -30,3 +30,13 @@ export function getMediaTypeByFileMimeType(mimetype: string) { if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE; return UmbMediaTypeFileType.FILE; } + +export function isMediaTypeRenderable(mediaTypeUnique: string) { + if (mediaTypeUnique === UmbMediaTypeFileType.IMAGE) return true; + if (mediaTypeUnique === UmbMediaTypeFileType.SVG) return true; + return false; +} + +export function isMediaTypeFolder(mediaTypeUnique: string) { + return mediaTypeUnique === UmbMediaTypeFileType.FOLDER; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts new file mode 100644 index 0000000000..ccae01905f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts @@ -0,0 +1 @@ +export * from './media-picker-folder-path.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts new file mode 100644 index 0000000000..5a97fac2cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts @@ -0,0 +1,164 @@ +import type { UmbMediaDetailModel } from '../../../types.js'; +import { UmbMediaDetailRepository } from '../../../repository/index.js'; +import { UmbMediaTreeRepository } from '../../../tree/media-tree.repository.js'; +import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from '../media-picker-modal.token.js'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbMediaTypeFileType } from '@umbraco-cms/backoffice/media-type'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; + +interface MediaPath { + name: string; + unique: string | null; +} + +const root = { name: 'Media', unique: null }; + +@customElement('umb-media-picker-folder-path') +export class UmbMediaPickerFolderPathElement extends UmbModalBaseElement< + UmbMediaPickerModalData, + UmbMediaPickerModalValue +> { + #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure + #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders + + @property() + public set currentPath(value: string | null) { + if (value !== this._currentPath) { + this._currentPath = value; + this.#loadPath(); + this.dispatchEvent(new UmbChangeEvent()); + } + } + public get currentPath() { + return this._currentPath; + } + + @state() + private _currentPath: string | null = null; + + @state() + private _paths: Array = [root]; + + @state() + private _typingNewFolder = false; + + connectedCallback(): void { + super.connectedCallback(); + this.#loadPath(); + } + + async #loadPath() { + if (this.currentPath) { + const { data } = await this.#mediaTreeRepository.requestTreeItemAncestors({ + descendantUnique: this.currentPath, + }); + if (data) { + this._paths = [root, ...data.map((item) => ({ name: item.name, unique: item.unique }))]; + } else { + this._paths = [root]; + } + } + } + + #goToFolder(unique: string | null) { + this._paths = [...this._paths].slice(0, this._paths.findIndex((path) => path.unique === unique) + 1); + this.currentPath = unique; + } + + #focusFolderInput() { + this._typingNewFolder = true; + requestAnimationFrame(() => { + const element = this.getHostElement().shadowRoot!.querySelector('#new-folder') as UUIInputElement; + element.focus(); + element.select(); + }); + } + + async #addFolder(e: UUIInputEvent) { + e.stopPropagation(); + const newName = e.target.value as string; + this._typingNewFolder = false; + if (!newName) return; + + const newUnique = UmbId.new(); + const parentUnique = this._paths[this._paths.length - 1].unique; + + const preset: Partial = { + unique: newUnique, + mediaType: { + unique: UmbMediaTypeFileType.FOLDER, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: newName, + createDate: null, + updateDate: null, + }, + ], + }; + const { data: scaffold } = await this.#mediaDetailRepository.createScaffold(preset); + if (!scaffold) return; + + const { data } = await this.#mediaDetailRepository.create(scaffold, parentUnique); + if (!data) return; + + const name = data.variants[0].name; + const unique = data.unique; + + this._paths = [...this._paths, { name, unique }]; + this.currentPath = unique; + } + + render() { + return html`
+ ${repeat( + this._paths, + (path) => path.unique, + (path) => + html` this.#goToFolder(path.unique)}>/`, + )}${this._typingNewFolder + ? html`` + : html`+`} +
`; + } + + static styles = [ + css` + #path { + display: flex; + align-items: center; + margin-bottom: var(--uui-size-3); + } + #path uui-button { + font-weight: bold; + } + #path uui-input { + height: 100%; + } + `, + ]; +} + +export default UmbMediaPickerFolderPathElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-folder-path': UmbMediaPickerFolderPathElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts index 0c0aa9d430..4b8bbe9334 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts @@ -1,2 +1,3 @@ +export * from './components/index.js'; export * from './media-picker-modal.element.js'; export * from './media-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 3ed4c59bbf..0f7ce2d114 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,62 +1,62 @@ -import type { UmbMediaDetailModel } from '../../types.js'; -import { UmbMediaDetailRepository } from '../../repository/index.js'; +import type { UmbMediaUrlModel } from '../../repository/url/types.js'; +import { + UmbMediaDetailRepository, + type UmbMediaItemModel, + UmbMediaItemRepository, + UmbMediaUrlRepository, +} from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import type { UmbMediaTreeItemModel } from '../../tree/types.js'; import type { UmbDropzoneMediaElement } from '../../components/index.js'; +import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { css, html, customElement, state, repeat, query } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbMediaTypeFileType } from '@umbraco-cms/backoffice/media-type'; +import { css, html, customElement, state, repeat, query, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { isMediaTypeFolder, isMediaTypeRenderable } from '@umbraco-cms/backoffice/media-type'; -// Folder media type unique from backend -const FOLDER = 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d'; - -interface MediaPath { - name: string; - unique: string | null; +interface MappedMediaItem + extends Partial, + Partial, + Partial { + isImageRenderable?: boolean; } @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { - #mediaTreeRepository = new UmbMediaTreeRepository(this); - #mediaDetailRepository = new UmbMediaDetailRepository(this); + #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure + #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders + #mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls + #mediaItemRepository = new UmbMediaItemRepository(this); // used to search + + #mediaItemsCurrentFolder: Array = []; @state() - private _items: Array = []; + private _mediaFilteredList: Array = []; @state() - private _selectableFolders = false; + private _searchOnlyThisFolder = false; @state() - private _paths: Array = [{ name: 'Media', unique: null }]; + private _searchQuery = ''; @state() private _currentPath: string | null = null; @state() - private _typingNewFolder = false; + private _selectableNonImages = true; + + @state() + private _selectableFolders = true; @query('#dropzone') private _dropzone!: UmbDropzoneMediaElement; connectedCallback(): void { super.connectedCallback(); - this._selectableFolders = this.data?.selectableFolders ?? false; + this._selectableNonImages = this.data?.selectableNonImages ?? true; + this._selectableFolders = this.data?.selectableFolders ?? true; this._currentPath = this.data?.startNode ?? null; - this.#loadPath(); - } - - async #loadPath() { - if (this._currentPath) { - const { data } = await this.#mediaTreeRepository.requestTreeItemAncestors({ - descendantUnique: this._currentPath, - }); - const paths = data?.map((item) => ({ name: item.name, unique: item.unique })) ?? []; - this._paths = [...this._paths, ...paths]; - } - this.#loadMediaFolder(); } @@ -67,82 +67,77 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement path.unique === unique) + 1); - this._currentPath = unique; - this.#loadMediaFolder(); - } + async #mapMediaUrls(items: Array): Promise> { + if (!items.length) return []; - #onFolderOpen(item: UmbMediaTreeItemModel) { - if (item.mediaType.unique !== FOLDER) return; - this._paths = [...this._paths, { name: item.name, unique: item.unique }]; - this._currentPath = item.unique; - this.#loadMediaFolder(); - } + const { data } = await this.#mediaUrlRepository.requestItems(items.map((item) => item.unique)); - #focusFolderInput() { - this._typingNewFolder = true; - requestAnimationFrame(() => { - const element = this.getHostElement().shadowRoot!.querySelector('#new-folder') as UUIInputElement; - element.focus(); - element.select(); + return items.map((item) => { + const media = data?.find((media) => media.unique === item.unique); + const isImageRenderable = isMediaTypeRenderable(item.mediaType.unique); + const isFolder = isMediaTypeFolder(item.mediaType.unique); + + return { ...item, ...media, isImageRenderable, isFolder }; }); } - async #addFolder(e: UUIInputEvent) { - const name = e.target.value as string; - - if (name) { - const unique = UmbId.new(); - const parentUnique = this._paths[this._paths.length - 1].unique; - this._paths = [...this._paths, { name, unique }]; - this._currentPath = unique; - - const preset: Partial = { - unique, - mediaType: { - unique: UmbMediaTypeFileType.FOLDER, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: name, - createDate: null, - updateDate: null, - }, - ], - }; - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (data) { - await this.#mediaDetailRepository.create(data, parentUnique); - } - } - this._typingNewFolder = false; + #onFolderOpen(item: MappedMediaItem) { + if (!isMediaTypeFolder(item.mediaType!.unique)) return; + this._currentPath = item.unique!; this.#loadMediaFolder(); } - #onSelected(item: UmbMediaTreeItemModel) { - const selection = this.data?.multiple ? [...this.value.selection, item.unique] : [item.unique]; + #onSelected(item: MappedMediaItem) { + const selection = this.data?.multiple ? [...this.value.selection, item.unique!] : [item.unique!]; this.modalContext?.setValue({ selection }); - if (this.data?.submitOnSelection) { - this._submitModal(); - } } - #onDeselected(item: UmbMediaTreeItemModel) { + #onDeselected(item: MappedMediaItem) { const selection = this.value.selection.filter((value) => value !== item.unique); this.modalContext?.setValue({ selection }); } + async #filterMediaItems() { + if (!this._searchQuery) { + // No search query, show all media items in current folder. + this._mediaFilteredList = this.#mediaItemsCurrentFolder; + return; + } + + const query = this._searchQuery; + const { data } = await this.#mediaItemRepository.search({ query, skip: 0, take: 100 }); + + const foundItems = data?.filter((found) => { + if (found.isTrashed) return false; + if (this._searchOnlyThisFolder) { + const isInFolder = this.#mediaItemsCurrentFolder.find((inFolder) => inFolder.unique === found.unique); + return isInFolder; + } + return true; + }); + + this._mediaFilteredList = await this.#mapMediaUrls(foundItems ?? []); + } + + #onSearch(e: UUIInputEvent) { + this._searchQuery = (e.target.value as string).toLocaleLowerCase(); + this.#filterMediaItems(); + } + + #onPathChange(e: CustomEvent) { + this._currentPath = (e.target as UmbMediaPickerFolderPathElement).currentPath; + this.#loadMediaFolder(); + } + render() { return html` @@ -160,14 +155,14 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#loadMediaFolder()} .parentUnique=${this._currentPath}> ${ - !this._items.length + !this._mediaFilteredList.length ? html`

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

` : html`
${repeat( - this._items, + this._mediaFilteredList, (item) => item.unique, (item) => this.#renderCard(item), )} @@ -176,48 +171,33 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement`; } - #renderToolbar() { - return html`
- - this._dropzone?.browse()}> -
- ${this.#renderPath()}`; + #renderPath() { + return html``; } - #renderPath() { - return html`
- ${repeat( - this._paths, - (path) => path.unique, - (path) => - html` this.#goToFolder(path.unique)}>/`, - )}${this._typingNewFolder - ? html`` - : html`+`} + #renderToolbar() { + return html`
+ + this._dropzone?.browse()}>
`; } - #renderCard(item: UmbMediaTreeItemModel) { + #renderCard(item: MappedMediaItem) { return html` this.#onSelected(item)} @deselected=${() => this.#onDeselected(item)} ?selected=${this.value?.selection?.find((value) => value === item.unique)} - ?selectable=${item.mediaType.unique !== FOLDER || this._selectableFolders} - ?select-only=${item.mediaType.unique !== FOLDER} - file-ext=${item.mediaType.unique !== FOLDER ? item.mediaType.icon : ''}> + ?selectable=${(!item.isFolder || this._selectableFolders) && + (item.isImageRenderable || this._selectableNonImages)} + ?select-only=${!item.isFolder} + file-ext=${ifDefined(item.extension)}> + ${item.isImageRenderable && item.url ? html`${ifDefined(item.name)}` : ''} `; } @@ -258,18 +240,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement undefined); - const mediaData = modalHandler?.getValue(); + const mediaData = await modalHandler?.onSubmit().catch(() => null); + if (!mediaData) return; const media: MediaPickerTargetData = { altText: mediaData?.altText, From 3286d557122628b6c157e6b664cd008157570d63 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:25:03 +0200 Subject: [PATCH 010/134] fix issue on dropzone where only images would get uploaded --- .../media/components/dropzone-media/dropzone-media.element.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts index 6f999a43a8..2a129deb8f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -7,6 +7,7 @@ import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository, getMediaTypeByFileMimeType, + UmbMediaTypeFileType, } from '@umbraco-cms/backoffice/media-type'; import { UmbTemporaryFileManager, @@ -101,6 +102,7 @@ export class UmbDropzoneMediaElement extends UmbLitElement { for (const upload of uploads) { const mediaType = this.#getMediaTypeFromMime(upload.file.type); + const value = mediaType.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; const preset: Partial = { mediaType: { @@ -120,7 +122,7 @@ export class UmbDropzoneMediaElement extends UmbLitElement { ? [ { alias: 'umbracoFile', - value: { src: upload.unique }, + value, culture: null, segment: null, }, From 4716c8021e5c166d7c2b3ddbb7ff3779b85d702e Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:21:28 +0200 Subject: [PATCH 011/134] dropzone management --- src/Umbraco.Web.UI.Client/package-lock.json | 3 +- src/Umbraco.Web.UI.Client/package.json | 2 + .../src/external/backend-api/src/models.ts | 77 +++-- .../src/external/backend-api/src/services.ts | 167 +++++++--- .../src/external/mime-types/index.ts | 1 + .../media-type-structure.repository.ts | 14 + ...media-type-structure.server.data-source.ts | 18 ++ .../packages/media/media-types/utils/index.ts | 31 +- .../dropzone/dropzone-manager.class.ts | 46 +++ .../components/dropzone/dropzone.element.ts | 302 ++++++++++++++++++ .../media/media/components/dropzone/index.ts | 2 + ...oadable-structure-data-source.interface.ts | 16 + ...pe-uploadable-structure-repository-base.ts | 32 ++ ...loadable-structure-repository.interface.ts | 7 + ...dable-structure-server-data-source-base.ts | 66 ++++ src/Umbraco.Web.UI.Client/tsconfig.json | 1 + 16 files changed, 691 insertions(+), 94 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3a51ab3741..42b2aa56f6 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -20,6 +20,7 @@ "element-internals-polyfill": "^1.3.10", "lit": "^3.1.2", "marked": "^12.0.0", + "mime-types": "^2.1.35", "monaco-editor": "^0.46.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", @@ -15849,7 +15850,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -15858,7 +15858,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 73eef9f83d..62eb0b7644 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -96,6 +96,7 @@ "./external/dompurify": "./dist-cms/external/dompurify/index.js", "./external/lit": "./dist-cms/external/lit/index.js", "./external/marked": "./dist-cms/external/marked/index.js", + "./external/mime-types": "./dist-cms/external/mime-types/index.js", "./external/monaco-editor": "./dist-cms/external/monaco-editor/index.js", "./external/openid": "./dist-cms/external/openid/index.js", "./external/router-slot": "./dist-cms/external/router-slot/index.js", @@ -179,6 +180,7 @@ "element-internals-polyfill": "^1.3.10", "lit": "^3.1.2", "marked": "^12.0.0", + "mime-types": "^2.1.35", "monaco-editor": "^0.46.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts index 1bcb75c327..1987ac421e 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts @@ -625,8 +625,7 @@ updater?: string | null }; export type DocumentConfigurationResponseModel = { - sanitizeTinyMce: boolean -disableDeleteWhenReferenced: boolean + disableDeleteWhenReferenced: boolean disableUnpublishWhenReferenced: boolean allowEditInvariantFromNonDefault: boolean allowNonExistingSegmentsCreation: boolean @@ -1124,7 +1123,6 @@ mediaType: MediaTypeCollectionReferenceResponseModel export type MediaConfigurationResponseModel = { disableDeleteWhenReferenced: boolean disableUnpublishWhenReferenced: boolean -sanitizeTinyMce: boolean reservedFieldNames: Array }; @@ -1788,6 +1786,11 @@ export type PagedSearcherResponseModel = { items: Array }; +export type PagedSegmentResponseModel = { + total: number +items: Array + }; + export type PagedTagResponseModel = { total: number items: Array @@ -1894,6 +1897,13 @@ memberUserNames: Array memberGroupNames: Array }; +export type PublicAccessResponseModel = { + loginDocument: ReferenceByIdModel +errorDocument: ReferenceByIdModel +members: Array +groups: Array + }; + export type PublishDocumentRequestModel = { publishSchedules: Array }; @@ -2052,6 +2062,11 @@ export type SecurityConfigurationResponseModel = { passwordConfiguration: PasswordConfigurationResponseModel }; +export type SegmentResponseModel = { + name: string +alias: string + }; + export type ServerConfigurationItemResponseModel = { name: string data: string @@ -3397,7 +3412,7 @@ take?: number ,PutDocumentByIdNotifications: string ,PostDocumentByIdPublicAccess: string ,DeleteDocumentByIdPublicAccess: string - ,GetDocumentByIdPublicAccess: void + ,GetDocumentByIdPublicAccess: PublicAccessResponseModel ,PutDocumentByIdPublicAccess: string ,PutDocumentByIdPublish: string ,PutDocumentByIdPublishWithDescendants: string @@ -3583,6 +3598,7 @@ requestBody?: UpdateLanguageRequestModel responses: { GetItemLanguage: Array + ,GetItemLanguageDefault: LanguageItemResponseModel ,GetLanguage: PagedLanguageResponseModel ,PostLanguage: string ,GetLanguageByIsoCode: LanguageResponseModel @@ -3681,6 +3697,12 @@ export type MediaTypeData = { GetItemMediaType: { id?: Array + }; +GetItemMediaTypeAllowed: { + fileExtension?: string +skip?: number +take?: number + }; GetItemMediaTypeSearch: { query?: string @@ -3773,6 +3795,7 @@ take?: number responses: { GetItemMediaType: Array + ,GetItemMediaTypeAllowed: PagedModelMediaTypeItemResponseModel ,GetItemMediaTypeSearch: PagedModelMediaTypeItemResponseModel ,PostMediaType: string ,GetMediaTypeById: MediaTypeResponseModel @@ -3996,10 +4019,10 @@ take?: number responses: { GetItemMemberGroup: Array ,GetMemberGroup: PagedMemberGroupResponseModel - ,PostMemberGroup: MemberGroupResponseModel + ,PostMemberGroup: string ,GetMemberGroupById: MemberGroupResponseModel ,DeleteMemberGroupById: string - ,PutMemberGroupById: MemberGroupResponseModel + ,PutMemberGroupById: string ,GetTreeMemberGroupRoot: PagedNamedEntityTreeItemResponseModel } @@ -4591,6 +4614,24 @@ PostSecurityForgotPasswordVerify: { } +export type SegmentData = { + + payloads: { + GetSegment: { + skip?: number +take?: number + + }; + } + + + responses: { + GetSegment: PagedSegmentResponseModel + + } + + } + export type ServerData = { @@ -4879,10 +4920,6 @@ export type UserDataData = { PostUserData: { requestBody?: CreateUserDataRequestModel - }; -PutUserData: { - requestBody?: UpdateUserDataRequestModel - }; GetUserData: { groups?: Array @@ -4890,6 +4927,10 @@ identifiers?: Array skip?: number take?: number + }; +PutUserData: { + requestBody?: UpdateUserDataRequestModel + }; GetUserDataById: { id: string @@ -4900,8 +4941,8 @@ GetUserDataById: { responses: { PostUserData: string - ,PutUserData: string ,GetUserData: PagedUserDataResponseModel + ,PutUserData: string ,GetUserDataById: UserDataModel } @@ -5152,7 +5193,11 @@ PostUserUnlock: { export type WebhookData = { payloads: { - GetWebhook: { + GetItemWebhook: { + id?: Array + + }; +GetWebhook: { skip?: number take?: number @@ -5173,21 +5218,17 @@ requestBody?: UpdateWebhookRequestModel DeleteWebhookById: { id: string - }; -GetWebhookItem: { - ids?: Array - }; } responses: { - GetWebhook: PagedWebhookResponseModel + GetItemWebhook: Array + ,GetWebhook: PagedWebhookResponseModel ,PostWebhook: string ,GetWebhookById: WebhookResponseModel ,PutWebhookById: string ,DeleteWebhookById: string - ,GetWebhookItem: Array } diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts index b00b6aad71..64e967c0c7 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts @@ -1,7 +1,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { AuditLogData, CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; +import type { AuditLogData, CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; export class AuditLogService { @@ -2220,6 +2220,7 @@ requestBody } /** + * @returns unknown Success * @throws ApiError */ public static getDocumentByIdPublicAccess(data: DocumentData['payloads']['GetDocumentByIdPublicAccess']): CancelablePromise { @@ -3129,6 +3130,22 @@ export class LanguageService { }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getItemLanguageDefault(): CancelablePromise { + + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/item/language/default', + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -3546,6 +3563,30 @@ export class MediaTypeService { }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getItemMediaTypeAllowed(data: MediaTypeData['payloads']['GetItemMediaTypeAllowed'] = {}): CancelablePromise { + const { + + fileExtension, +skip, +take + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/item/media-type/allowed', + query: { + fileExtension, skip, take + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -4679,7 +4720,7 @@ take } /** - * @returns unknown Success + * @returns string Created * @throws ApiError */ public static postMemberGroup(data: MemberGroupData['payloads']['PostMemberGroup'] = {}): CancelablePromise { @@ -4692,6 +4733,7 @@ take url: '/umbraco/management/api/v1/member-group', body: requestBody, mediaType: 'application/json', + responseHeader: 'Umb-Generated-Resource', errors: { 400: `Bad Request`, 401: `The resource is protected and requires an authentication token`, @@ -4749,7 +4791,7 @@ take } /** - * @returns unknown Success + * @returns string Success * @throws ApiError */ public static putMemberGroupById(data: MemberGroupData['payloads']['PutMemberGroupById']): CancelablePromise { @@ -4766,6 +4808,7 @@ requestBody }, body: requestBody, mediaType: 'application/json', + responseHeader: 'Umb-Notifications', errors: { 400: `Bad Request`, 401: `The resource is protected and requires an authentication token`, @@ -6756,6 +6799,34 @@ export class SecurityService { } +export class SegmentService { + + /** + * @returns unknown Success + * @throws ApiError + */ + public static getSegment(data: SegmentData['payloads']['GetSegment'] = {}): CancelablePromise { + const { + + skip, +take + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/segment', + query: { + skip, take + }, + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + +} + export class ServerService { /** @@ -7726,29 +7797,6 @@ export class UserDataService { }); } - /** - * @returns string Success - * @throws ApiError - */ - public static putUserData(data: UserDataData['payloads']['PutUserData'] = {}): CancelablePromise { - const { - - requestBody - } = data; - return __request(OpenAPI, { - method: 'PUT', - url: '/umbraco/management/api/v1/user-data', - body: requestBody, - mediaType: 'application/json', - responseHeader: 'Umb-Notifications', - errors: { - 400: `Bad Request`, - 401: `The resource is protected and requires an authentication token`, - 404: `Not Found`, - }, - }); - } - /** * @returns unknown Success * @throws ApiError @@ -7773,6 +7821,29 @@ take }); } + /** + * @returns string Success + * @throws ApiError + */ + public static putUserData(data: UserDataData['payloads']['PutUserData'] = {}): CancelablePromise { + const { + + requestBody + } = data; + return __request(OpenAPI, { + method: 'PUT', + url: '/umbraco/management/api/v1/user-data', + body: requestBody, + mediaType: 'application/json', + responseHeader: 'Umb-Notifications', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -8829,6 +8900,28 @@ requestBody export class WebhookService { + /** + * @returns unknown Success + * @throws ApiError + */ + public static getItemWebhook(data: WebhookData['payloads']['GetItemWebhook'] = {}): CancelablePromise { + const { + + id + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/item/webhook', + query: { + id + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -8950,26 +9043,4 @@ requestBody }); } - /** - * @returns unknown Success - * @throws ApiError - */ - public static getWebhookItem(data: WebhookData['payloads']['GetWebhookItem'] = {}): CancelablePromise { - const { - - ids - } = data; - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/webhook/item', - query: { - ids - }, - errors: { - 401: `The resource is protected and requires an authentication token`, - 403: `The authenticated user do not have access to this resource`, - }, - }); - } - } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts b/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts new file mode 100644 index 0000000000..38a5df465f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts @@ -0,0 +1 @@ +export * as mime from 'mime-types'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts index 0db7b421b7..86733391e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts @@ -4,8 +4,22 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type'; export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepositoryBase { + #dataSource; constructor(host: UmbControllerHost) { super(host, UmbMediaTypeStructureServerDataSource); + this.#dataSource = new UmbMediaTypeStructureServerDataSource(host); + } + + async requestMediaTypesOf({ + fileExtension, + skip = 0, + take = 100, + }: { + fileExtension: string; + skip?: number; + take?: number; + }) { + return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts index 22b833b379..acdfad98f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts @@ -17,6 +17,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu constructor(host: UmbControllerHost) { super(host, { getAllowedChildrenOf, mapper }); } + + getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) { + return getAllowedMediaTypesOfExtension({ fileExtension, skip, take }); + } } const getAllowedChildrenOf = (unique: string | null) => { @@ -37,3 +41,17 @@ const mapper = (item: AllowedMediaTypeModel): UmbAllowedMediaTypeModel => { icon: item.icon || null, }; }; + +const getAllowedMediaTypesOfExtension = async ({ + fileExtension, + skip, + take, +}: { + fileExtension: string; + skip: number; + take: number; +}) => { + // eslint-disable-next-line local-rules/no-direct-api-import + const { items } = await MediaTypeService.getItemMediaTypeAllowed({ fileExtension, skip, take }); + return items.map((item) => mapper(item)); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts index b67055a38e..2557bafb9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts @@ -1,28 +1,7 @@ -export enum UmbMediaTypeFileType { - SVG = 'Vector Graphics (SVG)', - IMAGE = 'Image', - AUDIO = 'Audio', - VIDEO = 'Video', - ARTICLE = 'Article', - FILE = 'File', -} +import { mime } from '@umbraco-cms/backoffice/external/mime-types'; -export function getMediaTypeByFileExtension(extension: string) { - if (extension === 'svg') return UmbMediaTypeFileType.SVG; - if (['jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff', 'tif', 'webp'].includes(extension)) - return UmbMediaTypeFileType.IMAGE; - if (['mp3', 'weba', 'oga', 'opus'].includes(extension)) return UmbMediaTypeFileType.AUDIO; - if (['mp4', 'webm', 'ogv'].includes(extension)) return UmbMediaTypeFileType.VIDEO; - if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE; - return UmbMediaTypeFileType.FILE; -} - -export function getMediaTypeByFileMimeType(mimetype: string) { - if (mimetype === 'image/svg+xml') return UmbMediaTypeFileType.SVG; - const [type, extension] = mimetype.split('/'); - if (type === 'image') return UmbMediaTypeFileType.IMAGE; - if (type === 'audio') return UmbMediaTypeFileType.AUDIO; - if (type === 'video') return UmbMediaTypeFileType.VIDEO; - if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE; - return UmbMediaTypeFileType.FILE; +export function getExtensionFromMime(mimeType: string): string | undefined { + const extension = mime.extension(mimeType); + if (!extension) return; // extension doesn't exist. + return extension; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts new file mode 100644 index 0000000000..6abcd410ba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts @@ -0,0 +1,46 @@ +import type { UmbContentTypeUploadableStructureRepositoryBase } from './repository/content-type-uploadable-structure-repository-base.js'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbMediaTypeItemModel } from '@umbraco-cms/backoffice/media-type'; +import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; + +export class UmbDropzoneManager extends UmbControllerBase { + #init!: Promise; + + #fileManager = new UmbTemporaryFileManager(this); + #repository: UmbContentTypeUploadableStructureRepositoryBase; + + #parentUnique: string | null = null; + + constructor( + host: UmbControllerHost, + typeRepository: UmbContentTypeUploadableStructureRepositoryBase, + parentUnique: string | null, + ) { + super(host); + this.#repository = typeRepository; + this.#parentUnique = parentUnique; + } + + public async dropOneFile(file: File) { + const matchingMediaTypes = await this.#repository.requestAllowedMediaTypesOf(file.type); + //const options = this.#allowedMediaTypes.filter((allowedMediaType) => matchingMediaTypes.includes(allowedMediaType)); + } + + public async dropFiles(files: Array) {} + + async #requestAllowedMediaTypesOf(fileExtension: string) { + const { data } = await this.#repository.requestAllowedMediaTypesOf(fileExtension); + //const mediaTypes = data?.filter((option) => this.#allowedMediaTypes.includes(option)); + //return { fileExtension, mediaTypes }; + } + + private _reset() { + // + } + + public destroy() { + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts new file mode 100644 index 0000000000..d4885ccbec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts @@ -0,0 +1,302 @@ +import { UmbMediaDetailRepository } from '../../repository/index.js'; +import type { UmbMediaDetailModel } from '../../types.js'; +import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { + type UmbAllowedMediaTypeModel, + UmbMediaTypeStructureRepository, + UmbMediaTypeDetailRepository, + getExtensionFromMime, +} from '@umbraco-cms/backoffice/media-type'; +import { + UmbTemporaryFileManager, + type UmbTemporaryFileQueueModel, + type UmbTemporaryFileModel, +} from '@umbraco-cms/backoffice/temporary-file'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; + +interface MediaTypeOptions { + fileExtension: string; + mediaTypes: Array; +} + +interface UploadableFile { + file: File; + mediaType: UmbAllowedMediaTypeModel; + regularUploadField?: boolean; + temporaryUnique?: string; +} + +@customElement('umb-dropzone') +export class UmbDropzoneElement extends UmbLitElement { + #fileManager = new UmbTemporaryFileManager(this); + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); + #mediaDetailRepository = new UmbMediaDetailRepository(this); + #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); + + #allowedMediaTypes: Array = []; + + @state() + private queue: Array = []; + + @property({ attribute: false }) + parentUnique: string | null = null; + + public browse() { + const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; + return element.browse(); + } + + async #getAllowedMediaTypes(): Promise { + const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.parentUnique); + return data?.items ?? []; + } + + async #getAllowedMediaTypesOf(fileExtension: string): Promise { + const options = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); + const mediaTypes = options.filter((option) => this.#allowedMediaTypes.includes(option)); + return { fileExtension, mediaTypes }; + } + + constructor() { + super(); + document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); + document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); + document.addEventListener('drop', this.#handleDrop.bind(this)); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + document.removeEventListener('dragenter', this.#handleDragEnter.bind(this)); + document.removeEventListener('dragleave', this.#handleDragLeave.bind(this)); + document.removeEventListener('drop', this.#handleDrop.bind(this)); + } + + #handleDragEnter() { + this.toggleAttribute('dragging', true); + } + + #handleDragLeave() { + this.toggleAttribute('dragging', false); + } + + #handleDrop(event: DragEvent) { + event.preventDefault(); + this.toggleAttribute('dragging', false); + } + + async #onDropFiles(event: UUIFileDropzoneEvent) { + // TODO Handle of folder uploads. + + const files: Array = event.detail.files; + if (!files.length) return; + + this.#allowedMediaTypes = await this.#getAllowedMediaTypes(); + if (!this.#allowedMediaTypes.length) return; + // If we have files that are not allowed to be uploaded, we show those in a dialog to the user? + + if (files.length === 1) { + this.#handleOneFile(files[0]); + } else { + this.#handleMultipleFiles(files); + } + } + + async #handleOneFile(file: File) { + const extension = getExtensionFromMime(file.type); + if (!extension) return; // Extension doesn't exist. + + const options = await this.#getAllowedMediaTypesOf(extension); + if (!options.mediaTypes.length) return; // File type not allowed in current dropzone. + + if (options.mediaTypes.length === 1) { + this.#uploadFiles([{ file, mediaType: options.mediaTypes[0] }]); + return; + } + + // Multiple options, show a dialog to the user to pick one. + //TODO: Implement dialog. + } + + async #handleMultipleFiles(files: Array) { + // removes duplicate file types so we don't call the endpoint unnecessarily for every file. + const types = [...new Set(files.map((file) => file.type))]; + const options: Array = []; + + for (const type of types) { + const extension = getExtensionFromMime(type); + if (!extension) return; // Extension doesn't exist. + + options.push(await this.#getAllowedMediaTypesOf(extension)); + } + + // We are just going to automatically pick the first possible media type option for now, but consider an option dialog in the future. + const uploadable: Array = []; + files.forEach((file) => { + const mediaType = options.find((option) => option.fileExtension === file.type)?.mediaTypes[0] ?? undefined; + if (mediaType) uploadable.push({ file, mediaType }); + }); + + this.#uploadFiles(uploadable); + } + + async #uploadFiles(uploadable: Array) { + const queue = uploadable.map( + (item): UmbTemporaryFileQueueModel => ({ file: item.file, unique: item.temporaryUnique }), + ); + + const uploaded = await this.#fileManager.upload(queue); + + for (const upload of uploaded) { + const mediaType = uploadable.find((item) => item.temporaryUnique === upload.unique)?.mediaType; + const value = mediaType?.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; + + if (!mediaType) return; + + const preset: Partial = { + mediaType: { + unique: mediaType.unique, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: upload.file.name, + createDate: null, + updateDate: null, + }, + ], + values: upload.file.type + ? [ + { + alias: 'umbracoFile', + value, + culture: null, + segment: null, + }, + ] + : [], + }; + + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + if (!data) return; + + await this.#mediaDetailRepository.create(data, this.parentUnique); + + this.dispatchEvent(new UmbChangeEvent()); + } + } + + async #uploadHandler(files: Array) { + //TODO: Folders uploaded via UUIDropzone are always empty. Investigate why. + + const folders = files.filter((item) => !item.type).map((file): UmbTemporaryFileQueueModel => ({ file })); + const mediaItems = files.filter((item) => item.type); + + const queue = mediaItems.map((file): UmbTemporaryFileQueueModel => ({ file })); + + const uploaded = await this.#fileManager.upload(queue); + return [...folders, ...uploaded]; + } + + async #onFileUpload(event: UUIFileDropzoneEvent) { + const files: Array = event.detail.files; + + if (!files.length) return; + const uploads = await this.#uploadHandler(files); + + for (const upload of uploads) { + const mediaType = /*this.#getMediaTypeFromMime(upload.file.type); */ '' as any; + const value = mediaType.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; + + const preset: Partial = { + mediaType: { + unique: mediaType.unique, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: upload.file.name, + createDate: null, + updateDate: null, + }, + ], + values: upload.file.type + ? [ + { + alias: 'umbracoFile', + value, + culture: null, + segment: null, + }, + ] + : [], + }; + + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + if (!data) return; + + await this.#mediaDetailRepository.create(data, this.parentUnique); + + this.dispatchEvent(new UmbChangeEvent()); + } + } + + render() { + return html``; + } + + static styles = [ + css` + :host([dragging]) #dropzone { + opacity: 1; + pointer-events: all; + } + + [dropzone] { + opacity: 0; + } + + #dropzone { + opacity: 0; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset: 0px; + z-index: 100; + backdrop-filter: opacity(1); /* Removes the built in blur effect */ + border-radius: var(--uui-border-radius); + overflow: clip; + border: 1px solid var(--uui-color-focus); + } + #dropzone:after { + content: ''; + display: block; + position: absolute; + inset: 0; + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-focus); + opacity: 0.2; + } + `, + ]; +} + +export default UmbDropzoneElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-dropzone': UmbDropzoneElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts new file mode 100644 index 0000000000..38624dcb34 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts @@ -0,0 +1,2 @@ +export * from './dropzone.element.js'; +export * from './dropzone-manager.class.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts new file mode 100644 index 0000000000..35422dfc25 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts @@ -0,0 +1,16 @@ +import type { + UmbContentTypeStructureDataSource, + UmbContentTypeStructureDataSourceConstructor, +} from '@umbraco-cms/backoffice/content-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbContentTypeUploadableStructureDataSourceConstructor + extends UmbContentTypeStructureDataSourceConstructor { + new (host: UmbControllerHost): UmbContentTypeUploadableStructureDataSource; +} + +export interface UmbContentTypeUploadableStructureDataSource + extends UmbContentTypeStructureDataSource { + getAllowedMediaTypesOf(fileExtension: string | null): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts new file mode 100644 index 0000000000..60805f94cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts @@ -0,0 +1,32 @@ +import type { + UmbContentTypeUploadableStructureDataSource, + UmbContentTypeUploadableStructureDataSourceConstructor, +} from './content-type-uploadable-structure-data-source.interface.js'; +import type { UmbContentTypeUploadableStructureRepository } from './content-type-uploadable-structure-repository.interface.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type'; + +export abstract class UmbContentTypeUploadableStructureRepositoryBase + extends UmbContentTypeStructureRepositoryBase + implements UmbContentTypeUploadableStructureRepository +{ + #structureSource: UmbContentTypeUploadableStructureDataSource; + + constructor( + host: UmbControllerHost, + structureSource: UmbContentTypeUploadableStructureDataSourceConstructor, + ) { + super(host, structureSource); + this.#structureSource = new structureSource(host); + } + + /** + * Returns a promise with the allowed media-types of a uploadable content type. + * @param {string} unique + * @return {*} + * @memberof UmbContentTypeUploadableStructureRepositoryBase + */ + requestAllowedMediaTypesOf(fileExtension: string | null) { + return this.#structureSource.getAllowedMediaTypesOf(fileExtension); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts new file mode 100644 index 0000000000..b25083e1ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts @@ -0,0 +1,7 @@ +import type { UmbContentTypeStructureRepository } from '@umbraco-cms/backoffice/content-type'; +import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbContentTypeUploadableStructureRepository + extends UmbContentTypeStructureRepository { + requestAllowedMediaTypesOf(unique: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts new file mode 100644 index 0000000000..31e110ee31 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts @@ -0,0 +1,66 @@ +import { + type UmbContentTypeStructureDataSource, + UmbContentTypeStructureServerDataSourceBase, + type UmbContentTypeStructureServerDataSourceBaseArgs, +} from '@umbraco-cms/backoffice/content-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +// Temp type +type AllowedContentTypeModel = { + id: string; + name: string; + description?: string | null; + icon?: string | null; +}; + +export interface UmbContentTypeUploadableStructureServerDataSourceBaseArgs< + ServerItemType extends AllowedContentTypeModel, + ClientItemType extends { unique: string }, +> extends UmbContentTypeStructureServerDataSourceBaseArgs { + getAllowedMediaTypesOf: (fileExtension: string | null) => Promise; +} + +export abstract class UmbContentTypeUploadableStructureServerDataSourceBase< + ServerItemType extends AllowedContentTypeModel, + ClientItemType extends { unique: string }, + > + extends UmbContentTypeStructureServerDataSourceBase + implements UmbContentTypeStructureDataSource +{ + #host; + #getAllowedChildrenOf; + #mapper; + + /** + * Creates an instance of UmbContentTypeStructureServerDataSourceBase. + * @param {UmbControllerHost} host + * @memberof UmbItemServerDataSourceBase + */ + constructor( + host: UmbControllerHost, + args: UmbContentTypeUploadableStructureServerDataSourceBaseArgs, + ) { + super(host, args); + this.#host = host; + this.#getAllowedChildrenOf = args.getAllowedChildrenOf; + this.#mapper = args.mapper; + } + + /** + * Returns a promise with the allowed content types for the given unique + * @param {string} unique + * @return {*} + * @memberof UmbContentTypeStructureServerDataSourceBase + */ + async getAllowedMediaTypesOf(fileExtension: string | null) { + const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAllowedChildrenOf(fileExtension)); + + if (data) { + const items = data.items.map((item) => this.#mapper(item)); + return { data: { items, total: data.total } }; + } + + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index af06940c67..03f8acb7a5 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -114,6 +114,7 @@ "@umbraco-cms/backoffice/external/dompurify": ["./src/external/dompurify/index.ts"], "@umbraco-cms/backoffice/external/lit": ["./src/external/lit/index.ts"], "@umbraco-cms/backoffice/external/marked": ["./src/external/marked/index.ts"], + "@umbraco-cms/backoffice/external/mime-types": ["./src/external/mime-types/index.ts"], "@umbraco-cms/backoffice/external/monaco-editor": ["./src/external/monaco-editor/index.ts"], "@umbraco-cms/backoffice/external/openid": ["./src/external/openid/index.ts"], "@umbraco-cms/backoffice/external/router-slot": ["./src/external/router-slot/index.ts"], From 87bafe1d01d83fd8b013f7c445e29a33a33562e6 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 1 May 2024 13:55:49 +0200 Subject: [PATCH 012/134] moving files --- .../src/external/backend-api/src/models.ts | 132 ++++++------ .../src/external/backend-api/src/services.ts | 198 ++++++++++-------- .../src/external/mime-types/index.ts | 2 +- .../src/packages/media/media-types/index.ts | 2 - .../packages/media/media-types/utils/index.ts | 7 - .../collection/media-collection.element.ts | 2 +- .../dropzone-media/dropzone-media.element.ts | 19 +- .../dropzone/dropzone-manager.class.ts | 46 ---- .../media/dropzone/dropzone-manager.class.ts | 146 +++++++++++++ .../dropzone/dropzone.element.ts | 34 +-- .../media/{components => }/dropzone/index.ts | 0 .../media/media/dropzone/manifests.ts | 1 + ...ropzone-media-type-picker-modal.element.ts | 55 +++++ .../dropzone-media-type-picker-modal.token.ts | 19 ++ .../dropzone-media-type-picker/index.ts | 2 + .../media/media/dropzone/modals/index.ts | 1 + .../media/media/dropzone/modals/manifests.ts | 12 ++ ...oadable-structure-data-source.interface.ts | 0 ...pe-uploadable-structure-repository-base.ts | 0 ...loadable-structure-repository.interface.ts | 0 ...dable-structure-server-data-source-base.ts | 0 .../src/packages/media/media/index.ts | 1 + .../src/packages/media/media/manifests.ts | 2 + 23 files changed, 450 insertions(+), 231 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/dropzone.element.ts (92%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/index.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/repository/content-type-uploadable-structure-repository-base.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/repository/content-type-uploadable-structure-repository.interface.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{components => }/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts (100%) diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts index 1987ac421e..0f950e5d28 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts @@ -14,31 +14,14 @@ description?: string | null icon?: string | null }; -export type AuditLogEntityModel = { - id?: string | null -type?: string | null - }; - export type AuditLogResponseModel = { user: ReferenceByIdModel -entity?: AuditLogEntityModel | null timestamp: string logType: AuditTypeModel comment?: string | null parameters?: string | null }; -export type AuditLogWithUsernameResponseModel = { - user: ReferenceByIdModel -entity?: AuditLogEntityModel | null -timestamp: string -logType: AuditTypeModel -comment?: string | null -parameters?: string | null -userName?: string | null -userAvatars: Array - }; - export enum AuditTypeModel { NEW = 'New', SAVE = 'Save', @@ -130,7 +113,7 @@ export type CopyMediaTypeRequestModel = { export type CreateDataTypeRequestModel = { name: string editorAlias: string -editorUiAlias?: string | null +editorUiAlias: string values: Array id?: string | null parent?: ReferenceByIdModel | null @@ -496,7 +479,7 @@ properties: Array export type DataTypeResponseModel = { name: string editorAlias: string -editorUiAlias?: string | null +editorUiAlias: string values: Array id: string isDeletable: boolean @@ -1396,6 +1379,14 @@ id: string compositions: Array }; +export type MemberTypeTreeItemResponseModel = { + hasChildren: boolean +id: string +parent?: ReferenceByIdModel | null +name: string +icon: string + }; + export type MemberValueModel = { culture?: string | null segment?: string | null @@ -1477,6 +1468,10 @@ category: string type: EventMessageTypeModel }; +export type OEmbedResponseModel = { + markup: string + }; + export type ObjectTypeResponseModel = { name?: string | null id: string @@ -1546,11 +1541,6 @@ export type PagedAuditLogResponseModel = { items: Array }; -export type PagedAuditLogWithUsernameResponseModel = { - total: number -items: Array - }; - export type PagedCultureReponseModel = { total: number items: Array @@ -1681,6 +1671,11 @@ export type PagedMemberResponseModel = { items: Array }; +export type PagedMemberTypeTreeItemResponseModel = { + total: number +items: Array + }; + export type PagedModelDataTypeItemResponseModel = { items: Array total: number @@ -2268,7 +2263,7 @@ export type UnpublishDocumentRequestModel = { export type UpdateDataTypeRequestModel = { name: string editorAlias: string -editorUiAlias?: string | null +editorUiAlias: string values: Array }; @@ -2703,43 +2698,6 @@ id: string events: Array }; -export type AuditLogData = { - - payloads: { - GetAuditLog: { - orderDirection?: DirectionModel -sinceDate?: string -skip?: number -take?: number - - }; -GetAuditLogById: { - id: string -orderDirection?: DirectionModel -sinceDate?: string -skip?: number -take?: number - - }; -GetAuditLogTypeByLogType: { - logType: AuditTypeModel -sinceDate?: string -skip?: number -take?: number - - }; - } - - - responses: { - GetAuditLog: PagedAuditLogWithUsernameResponseModel - ,GetAuditLogById: PagedAuditLogResponseModel - ,GetAuditLogTypeByLogType: PagedAuditLogResponseModel - - } - - } - export type CultureData = { payloads: { @@ -3242,6 +3200,14 @@ PutDocumentById: { id: string requestBody?: UpdateDocumentRequestModel + }; +GetDocumentByIdAuditLog: { + id: string +orderDirection?: DirectionModel +sinceDate?: string +skip?: number +take?: number + }; PostDocumentByIdCopy: { id: string @@ -3403,6 +3369,7 @@ take?: number ,GetDocumentById: DocumentResponseModel ,DeleteDocumentById: string ,PutDocumentById: string + ,GetDocumentByIdAuditLog: PagedAuditLogResponseModel ,PostDocumentByIdCopy: string ,GetDocumentByIdDomains: DomainsResponseModel ,PutDocumentByIdDomains: string @@ -3858,6 +3825,14 @@ PutMediaById: { id: string requestBody?: UpdateMediaRequestModel + }; +GetMediaByIdAuditLog: { + id: string +orderDirection?: DirectionModel +sinceDate?: string +skip?: number +take?: number + }; PutMediaByIdMove: { id: string @@ -3955,6 +3930,7 @@ take?: number ,GetMediaById: MediaResponseModel ,DeleteMediaById: string ,PutMediaById: string + ,GetMediaByIdAuditLog: PagedAuditLogResponseModel ,PutMediaByIdMove: string ,PutMediaByIdMoveToRecycleBin: string ,GetMediaByIdReferencedBy: PagedIReferenceResponseModel @@ -4089,7 +4065,7 @@ take?: number ,GetMemberTypeByIdCompositionReferences: Array ,PostMemberTypeByIdCopy: string ,PostMemberTypeAvailableCompositions: Array - ,GetTreeMemberTypeRoot: PagedNamedEntityTreeItemResponseModel + ,GetTreeMemberTypeRoot: PagedMemberTypeTreeItemResponseModel } @@ -4195,6 +4171,25 @@ take?: number } +export type OembedData = { + + payloads: { + GetOembedQuery: { + maxHeight?: number +maxWidth?: number +url?: string + + }; + } + + + responses: { + GetOembedQuery: OEmbedResponseModel + + } + + } + export type PackageData = { payloads: { @@ -4952,7 +4947,13 @@ GetUserDataById: { export type UserGroupData = { payloads: { - GetItemUserGroup: { + GetFilterUserGroup: { + filter?: string +skip?: number +take?: number + + }; +GetItemUserGroup: { id?: Array }; @@ -4996,7 +4997,8 @@ requestBody?: Array responses: { - GetItemUserGroup: Array + GetFilterUserGroup: PagedUserGroupResponseModel + ,GetItemUserGroup: Array ,DeleteUserGroup: string ,PostUserGroup: string ,GetUserGroup: PagedUserGroupResponseModel diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts index 64e967c0c7..476f2ab3c6 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts @@ -1,91 +1,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { AuditLogData, CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; - -export class AuditLogService { - - /** - * @returns unknown Success - * @throws ApiError - */ - public static getAuditLog(data: AuditLogData['payloads']['GetAuditLog'] = {}): CancelablePromise { - const { - - orderDirection, -sinceDate, -skip, -take - } = data; - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/audit-log', - query: { - orderDirection, sinceDate, skip, take - }, - errors: { - 401: `The resource is protected and requires an authentication token`, - }, - }); - } - - /** - * @returns unknown Success - * @throws ApiError - */ - public static getAuditLogById(data: AuditLogData['payloads']['GetAuditLogById']): CancelablePromise { - const { - - id, -orderDirection, -sinceDate, -skip, -take - } = data; - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/audit-log/{id}', - path: { - id - }, - query: { - orderDirection, sinceDate, skip, take - }, - errors: { - 401: `The resource is protected and requires an authentication token`, - 403: `The authenticated user do not have access to this resource`, - }, - }); - } - - /** - * @returns unknown Success - * @throws ApiError - */ - public static getAuditLogTypeByLogType(data: AuditLogData['payloads']['GetAuditLogTypeByLogType']): CancelablePromise { - const { - - logType, -sinceDate, -skip, -take - } = data; - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/audit-log/type/{logType}', - path: { - logType - }, - query: { - sinceDate, skip, take - }, - errors: { - 401: `The resource is protected and requires an authentication token`, - }, - }); - } - -} +import type { CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, OembedData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; export class CultureService { @@ -1987,6 +1903,35 @@ requestBody }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getDocumentByIdAuditLog(data: DocumentData['payloads']['GetDocumentByIdAuditLog']): CancelablePromise { + const { + + id, +orderDirection, +sinceDate, +skip, +take + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/document/{id}/audit-log', + path: { + id + }, + query: { + orderDirection, sinceDate, skip, take + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns string Created * @throws ApiError @@ -4213,6 +4158,35 @@ requestBody }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getMediaByIdAuditLog(data: MediaData['payloads']['GetMediaByIdAuditLog']): CancelablePromise { + const { + + id, +orderDirection, +sinceDate, +skip, +take + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/media/{id}/audit-log', + path: { + id + }, + query: { + orderDirection, sinceDate, skip, take + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns string Success * @throws ApiError @@ -5414,6 +5388,34 @@ take } +export class OEmbedService { + + /** + * @returns unknown Success + * @throws ApiError + */ + public static getOembedQuery(data: OembedData['payloads']['GetOembedQuery'] = {}): CancelablePromise { + const { + + url, +maxWidth, +maxHeight + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/oembed/query', + query: { + url, maxWidth, maxHeight + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + +} + export class PackageService { /** @@ -7870,6 +7872,32 @@ take export class UserGroupService { + /** + * @returns unknown Success + * @throws ApiError + */ + public static getFilterUserGroup(data: UserGroupData['payloads']['GetFilterUserGroup'] = {}): CancelablePromise { + const { + + skip, +take, +filter + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/filter/user-group', + query: { + skip, take, filter + }, + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts b/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts index 38a5df465f..44b10e99aa 100644 --- a/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts @@ -1 +1 @@ -export * as mime from 'mime-types'; +export { extension as mimeToExtension } from 'mime-types'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts index e746aa874e..94205d2f84 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts @@ -7,6 +7,4 @@ export * from './repository/index.js'; export * from './tree/types.js'; export * from './types.js'; -export * from './utils/index.js'; - export { UMB_MEDIA_TYPE_PICKER_MODAL } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts deleted file mode 100644 index 2557bafb9c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { mime } from '@umbraco-cms/backoffice/external/mime-types'; - -export function getExtensionFromMime(mimeType: string): string | undefined { - const extension = mime.extension(mimeType); - if (!extension) return; // extension doesn't exist. - return extension; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index dd74cf2241..d3e7300801 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -32,7 +32,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { return html` ${when(this._progress >= 0, () => html``)} - + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts index 6a03dc5e19..12e32b5490 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -3,11 +3,7 @@ import type { UmbMediaDetailModel } from '../../types.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { - type UmbAllowedMediaTypeModel, - UmbMediaTypeStructureRepository, - getMediaTypeByFileMimeType, -} from '@umbraco-cms/backoffice/media-type'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; import { UmbTemporaryFileManager, type UmbTemporaryFileQueueModel, @@ -66,11 +62,12 @@ export class UmbDropzoneMediaElement extends UmbLitElement { if (!data) return; this.#allowedMediaTypes = data.items; } - - #getMediaTypeFromMime(mimetype: string): UmbAllowedMediaTypeModel { - const mediaTypeName = getMediaTypeByFileMimeType(mimetype); - return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; + /* + #getMediaTypeFromMime(mimetype: string): UmbAllowedMediaTypeModel | undefined { + //const mediaTypeName = getMediaTypeByFileMimeType(mimetype); + //return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; } + */ async #uploadHandler(files: Array) { const queue = files.map((file): UmbTemporaryFileQueueModel => ({ file })); @@ -84,11 +81,11 @@ export class UmbDropzoneMediaElement extends UmbLitElement { const uploads = await this.#uploadHandler(files); for (const upload of uploads) { - const mediaType = this.#getMediaTypeFromMime(upload.file.type); + //const mediaType = this.#getMediaTypeFromMime(upload.file.type); const preset: Partial = { mediaType: { - unique: mediaType.unique, + unique: /*mediaType.unique*/ '123', collection: null, }, variants: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts deleted file mode 100644 index 6abcd410ba..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone-manager.class.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { UmbContentTypeUploadableStructureRepositoryBase } from './repository/content-type-uploadable-structure-repository-base.js'; -import { UmbId } from '@umbraco-cms/backoffice/id'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbMediaTypeItemModel } from '@umbraco-cms/backoffice/media-type'; -import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; - -export class UmbDropzoneManager extends UmbControllerBase { - #init!: Promise; - - #fileManager = new UmbTemporaryFileManager(this); - #repository: UmbContentTypeUploadableStructureRepositoryBase; - - #parentUnique: string | null = null; - - constructor( - host: UmbControllerHost, - typeRepository: UmbContentTypeUploadableStructureRepositoryBase, - parentUnique: string | null, - ) { - super(host); - this.#repository = typeRepository; - this.#parentUnique = parentUnique; - } - - public async dropOneFile(file: File) { - const matchingMediaTypes = await this.#repository.requestAllowedMediaTypesOf(file.type); - //const options = this.#allowedMediaTypes.filter((allowedMediaType) => matchingMediaTypes.includes(allowedMediaType)); - } - - public async dropFiles(files: Array) {} - - async #requestAllowedMediaTypesOf(fileExtension: string) { - const { data } = await this.#repository.requestAllowedMediaTypesOf(fileExtension); - //const mediaTypes = data?.filter((option) => this.#allowedMediaTypes.includes(option)); - //return { fileExtension, mediaTypes }; - } - - private _reset() { - // - } - - public destroy() { - super.destroy(); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts new file mode 100644 index 0000000000..f8885f58b1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -0,0 +1,146 @@ +import { UmbMediaDetailRepository } from '../repository/index.js'; +import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; +import { mimeToExtension } from '@umbraco-cms/backoffice/external/mime-types'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { + type UmbAllowedMediaTypeModel, + UmbMediaTypeDetailRepository, + UmbMediaTypeStructureRepository, +} from '@umbraco-cms/backoffice/media-type'; +import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +interface UmbUploadableFileModel { + unique: string; + file: File; + mediaTypeUnique: string; +} + +interface UmbUploadableFileExtensionModel { + fileExtension: string; + mediaTypes: Array; +} + +export function getExtensionFromMime(mimeType: string): string | undefined { + const extension = mimeToExtension(mimeType); + if (!extension) return; // extension doesn't exist. + return extension; +} + +export class UmbDropzoneManager extends UmbControllerBase { + #host; + #tempFileManager = new UmbTemporaryFileManager(this); + + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); + #mediaDetailRepository = new UmbMediaDetailRepository(this); + #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); + + #parentUnique: string | null; + + #getExtensionFromMimeType(mimeType: string): string { + return getExtensionFromMime(mimeType) || ''; + } + + async #buildOptionsArrayFrom(fileExtensions: Array): Promise> { + // Getting all media types allowed in our current position based on parent unique. + const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.#parentUnique); + if (!allAllowedMediaTypes?.items.length) return []; + + const allowedByParent = allAllowedMediaTypes.items; + + // Building an array of options the files can be uploaded as. + const options: Array = []; + + for (const fileExtension of fileExtensions) { + const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); + const mediaTypes = extensionOptions.filter((option) => allowedByParent.includes(option)); + options.push({ fileExtension, mediaTypes }); + } + + return options; + } + + constructor(host: UmbControllerHost, parentUnique: string | null) { + super(host); + this.#host = host; + this.#parentUnique = parentUnique; + } + + async #showDialogMediaTypePicker(options: Array) { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); + const value = await modalContext.onSubmit().catch(() => undefined); + + return value?.mediaTypeUnique; + } + + public async dropOneFile(file: File) { + const extension = this.#getExtensionFromMimeType(file.type); + if (!extension) return; // Extension doesn't exist. + + const optionsArray = await this.#buildOptionsArrayFrom([extension]); + if (!optionsArray.length) return; // File not allowed in current dropzone. + + const mediaTypes = optionsArray[0].mediaTypes; // Because we are only uploading one file. + + if (mediaTypes.length === 1) { + // Only one allowed option, upload file using that option. + const uploadableFile: UmbUploadableFileModel = { + unique: UmbId.new(), + file, + mediaTypeUnique: mediaTypes[0].unique, + }; + + console.log('you made it!', uploadableFile); + return; + } + + // Multiple options, show a dialog for the user to pick one. + const mediaType = await this.#showDialogMediaTypePicker(mediaTypes); + if (!mediaType) return; // Upload cancelled. + + const uploadableFile: UmbUploadableFileModel = { + unique: UmbId.new(), + file, + mediaTypeUnique: mediaType, + }; + console.log('you made it!', uploadableFile); + } + + public async dropFiles(files: Array) { + // removes duplicate file types so we don't call endpoints unnecessarily when building options. + const mimeTypes = [...new Set(files.map((file) => file.type))]; + const optionsArray = await this.#buildOptionsArrayFrom( + mimeTypes.map((mimetype) => this.#getExtensionFromMimeType(mimetype)), + ); + + if (!optionsArray.length) return; // None of the files are allowed in current dropzone. + + // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones? + const uploadableFiles: Array = []; + + for (const file of files) { + const extension = this.#getExtensionFromMimeType(file.type); + const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; + + if (!options) return; // Dropped file not allowed in current dropzone. + + // Since we are uploading multiple files, we will pick first allowed option. + // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? + const mediaType = options[0]; + uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); + } + + console.log('you made it!', uploadableFiles); + } + + private _reset() { + // + } + + public destroy() { + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts similarity index 92% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index d4885ccbec..96f6e638a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -1,5 +1,6 @@ -import { UmbMediaDetailRepository } from '../../repository/index.js'; -import type { UmbMediaDetailModel } from '../../types.js'; +import { UmbMediaDetailRepository } from '../repository/index.js'; +import type { UmbMediaDetailModel } from '../types.js'; +import { UmbDropzoneManager } from './dropzone-manager.class.js'; import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -7,7 +8,6 @@ import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository, UmbMediaTypeDetailRepository, - getExtensionFromMime, } from '@umbraco-cms/backoffice/media-type'; import { UmbTemporaryFileManager, @@ -35,13 +35,22 @@ export class UmbDropzoneElement extends UmbLitElement { #mediaDetailRepository = new UmbMediaDetailRepository(this); #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); + #parentUnique: string | null = null; + #dropzoneManager = new UmbDropzoneManager(this, null); + #allowedMediaTypes: Array = []; @state() private queue: Array = []; @property({ attribute: false }) - parentUnique: string | null = null; + public set parentUnique(value: string | null) { + this.#parentUnique = value; + //this.#dropzoneManager.setParentUnique(value); + } + public get parentUnique(): string | null { + return this.#parentUnique; + } public browse() { const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; @@ -92,17 +101,13 @@ export class UmbDropzoneElement extends UmbLitElement { const files: Array = event.detail.files; if (!files.length) return; - this.#allowedMediaTypes = await this.#getAllowedMediaTypes(); - if (!this.#allowedMediaTypes.length) return; - // If we have files that are not allowed to be uploaded, we show those in a dialog to the user? - if (files.length === 1) { - this.#handleOneFile(files[0]); + this.#dropzoneManager.dropOneFile(files[0]); } else { - this.#handleMultipleFiles(files); + this.#dropzoneManager.dropFiles(files); } } - + /* async #handleOneFile(file: File) { const extension = getExtensionFromMime(file.type); if (!extension) return; // Extension doesn't exist. @@ -201,6 +206,7 @@ export class UmbDropzoneElement extends UmbLitElement { return [...folders, ...uploaded]; } + async #onFileUpload(event: UUIFileDropzoneEvent) { const files: Array = event.detail.files; @@ -208,7 +214,7 @@ export class UmbDropzoneElement extends UmbLitElement { const uploads = await this.#uploadHandler(files); for (const upload of uploads) { - const mediaType = /*this.#getMediaTypeFromMime(upload.file.type); */ '' as any; + const mediaType = this.#getMediaTypeFromMime(upload.file.type); const value = mediaType.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; const preset: Partial = { @@ -246,11 +252,13 @@ export class UmbDropzoneElement extends UmbLitElement { } } + */ + render() { return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts new file mode 100644 index 0000000000..c777b79000 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts @@ -0,0 +1 @@ +export * from './modals/manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts new file mode 100644 index 0000000000..8180a3df83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts @@ -0,0 +1,55 @@ +import type { + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue, +} from './dropzone-media-type-picker-modal.token.js'; +import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; + +@customElement('umb-dropzone-media-type-picker-modal') +export class UmbDropzoneMediaTypePickerModalElement extends UmbModalBaseElement< + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue +> { + @state() + private _options: Array = []; + + connectedCallback() { + super.connectedCallback(); + this._options = this.data?.options ?? []; + } + + #onMediaTypePick(unique?: string) { + this.value = { mediaTypeUnique: unique }; + this._submitModal(); + } + + render() { + return html` this.#onMediaTypePick()} label="Automatically" compact> + + ${repeat( + this._options, + (option) => option.unique, + (option) => + html` this.#onMediaTypePick(option.unique)} + label=${option.name} + compact> + + `, + )}`; + } + + static styles = [UmbTextStyles, css``]; +} + +export default UmbDropzoneMediaTypePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-dropzone-media-type-picker-modal': UmbDropzoneMediaTypePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts new file mode 100644 index 0000000000..4cd4a52974 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts @@ -0,0 +1,19 @@ +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDropzoneMediaTypePickerModalData { + options: Array; +} + +export type UmbDropzoneMediaTypePickerModalValue = { + mediaTypeUnique: string | undefined; +}; + +export const UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL = new UmbModalToken< + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue +>('Umb.Modal.Dropzone.MediaTypePicker', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts new file mode 100644 index 0000000000..b698197915 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts @@ -0,0 +1,2 @@ +export * from './dropzone-media-type-picker-modal.element.js'; +export * from './dropzone-media-type-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts new file mode 100644 index 0000000000..cd9b3459fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts @@ -0,0 +1 @@ +export * from './dropzone-media-type-picker/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts new file mode 100644 index 0000000000..e6500d5a4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.Dropzone.MediaTypePicker', + name: 'Dropzone Media Type Picker Modal', + js: () => import('./dropzone-media-type-picker/dropzone-media-type-picker-modal.element.js'), + }, +]; + +export const manifests: Array = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository-base.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-repository.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts 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..24f307b2f6 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 @@ -4,6 +4,7 @@ export * from './repository/index.js'; export * from './workspace/index.js'; export * from './reference/index.js'; export * from './components/index.js'; +export * from './dropzone/index.js'; export * from './entity.js'; export * from './utils/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index 1d355b70f6..61ae7c5407 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -1,4 +1,5 @@ import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as dropzoneManifests } from './dropzone/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; @@ -12,6 +13,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ ...collectionManifests, + ...dropzoneManifests, ...entityActionsManifests, ...entityBulkActionsManifests, ...menuManifests, From e713fc83bc973eb58660318953c16d8f773c8d1d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 1 May 2024 16:19:04 +0200 Subject: [PATCH 013/134] modal --- .../media/dropzone/dropzone-manager.class.ts | 104 +++++++++++------- ...ropzone-media-type-picker-modal.element.ts | 45 ++++++-- .../dropzone-media-type-picker-modal.token.ts | 1 + 3 files changed, 104 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index f8885f58b1..f9caed9882 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -18,7 +18,7 @@ interface UmbUploadableFileModel { mediaTypeUnique: string; } -interface UmbUploadableFileExtensionModel { +export interface UmbUploadableFileExtensionModel { fileExtension: string; mediaTypes: Array; } @@ -39,52 +39,32 @@ export class UmbDropzoneManager extends UmbControllerBase { #parentUnique: string | null; - #getExtensionFromMimeType(mimeType: string): string { - return getExtensionFromMime(mimeType) || ''; - } - - async #buildOptionsArrayFrom(fileExtensions: Array): Promise> { - // Getting all media types allowed in our current position based on parent unique. - const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.#parentUnique); - if (!allAllowedMediaTypes?.items.length) return []; - - const allowedByParent = allAllowedMediaTypes.items; - - // Building an array of options the files can be uploaded as. - const options: Array = []; - - for (const fileExtension of fileExtensions) { - const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); - const mediaTypes = extensionOptions.filter((option) => allowedByParent.includes(option)); - options.push({ fileExtension, mediaTypes }); - } - - return options; - } - constructor(host: UmbControllerHost, parentUnique: string | null) { super(host); this.#host = host; this.#parentUnique = parentUnique; } - async #showDialogMediaTypePicker(options: Array) { - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); - const value = await modalContext.onSubmit().catch(() => undefined); - - return value?.mediaTypeUnique; + public setParentUnique(parentUnique: string | null) { + this.#parentUnique = parentUnique; + } + public getParentUnique() { + return this.#parentUnique; } public async dropOneFile(file: File) { const extension = this.#getExtensionFromMimeType(file.type); - if (!extension) return; // Extension doesn't exist. + + if (!extension) { + // Folders have no extension on file drop. We assume it is a folder being uploaded. + this.#handleFolder(file); + return; + } const optionsArray = await this.#buildOptionsArrayFrom([extension]); - if (!optionsArray.length) return; // File not allowed in current dropzone. - - const mediaTypes = optionsArray[0].mediaTypes; // Because we are only uploading one file. + if (!optionsArray.length) throw new Error('File not allowed here.'); // Parent does not allow this file type here. + const mediaTypes = optionsArray[0].mediaTypes; if (mediaTypes.length === 1) { // Only one allowed option, upload file using that option. const uploadableFile: UmbUploadableFileModel = { @@ -93,7 +73,7 @@ export class UmbDropzoneManager extends UmbControllerBase { mediaTypeUnique: mediaTypes[0].unique, }; - console.log('you made it!', uploadableFile); + await this.#uploadOne(uploadableFile); return; } @@ -104,9 +84,9 @@ export class UmbDropzoneManager extends UmbControllerBase { const uploadableFile: UmbUploadableFileModel = { unique: UmbId.new(), file, - mediaTypeUnique: mediaType, + mediaTypeUnique: mediaType.unique, }; - console.log('you made it!', uploadableFile); + await this.#uploadOne(uploadableFile); } public async dropFiles(files: Array) { @@ -123,6 +103,11 @@ export class UmbDropzoneManager extends UmbControllerBase { for (const file of files) { const extension = this.#getExtensionFromMimeType(file.type); + if (!extension) { + // Folders have no extension on file drop. We assume it is a folder being uploaded. + this.#handleFolder(file); + return; + } const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; if (!options) return; // Dropped file not allowed in current dropzone. @@ -133,7 +118,50 @@ export class UmbDropzoneManager extends UmbControllerBase { uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); } - console.log('you made it!', uploadableFiles); + await this.#upload(uploadableFiles); + } + + #getExtensionFromMimeType(mimeType: string): string { + return getExtensionFromMime(mimeType) || ''; + } + + async #buildOptionsArrayFrom(fileExtensions: Array): Promise> { + // Getting all media types allowed in our current position based on parent unique. + const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.#parentUnique); + if (!allAllowedMediaTypes?.items.length) return []; + + const allowedByParent = allAllowedMediaTypes.items; + + // Building an array of options the files can be uploaded as. + const options: Array = []; + + for (const fileExtension of fileExtensions) { + const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); + const mediaTypes = extensionOptions.filter((option) => { + return allowedByParent.find((allowed) => option.unique === allowed.unique); + }); + options.push({ fileExtension, mediaTypes }); + } + return options; + } + + async #showDialogMediaTypePicker(options: Array) { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); + const value = await modalContext.onSubmit().catch(() => undefined); + return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null; + } + + async #upload(files: Array) { + await this.#tempFileManager.upload(files); + } + + async #uploadOne(file: UmbUploadableFileModel) { + await this.#tempFileManager.uploadOne(file); + } + + async #handleFolder(file: File) { + throw new Error('Not implemented: Folders coming soon!'); } private _reset() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts index 8180a3df83..859a8d77cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts @@ -2,10 +2,11 @@ import type { UmbDropzoneMediaTypePickerModalData, UmbDropzoneMediaTypePickerModalValue, } from './dropzone-media-type-picker-modal.token.js'; -import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, query, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-dropzone-media-type-picker-modal') export class UmbDropzoneMediaTypePickerModalElement extends UmbModalBaseElement< @@ -15,20 +16,30 @@ export class UmbDropzoneMediaTypePickerModalElement extends UmbModalBaseElement< @state() private _options: Array = []; + @query('#auto') + private _buttonAuto!: UUIButtonElement; + connectedCallback() { super.connectedCallback(); this._options = this.data?.options ?? []; + requestAnimationFrame(() => this._buttonAuto.focus()); } - #onMediaTypePick(unique?: string) { + #onMediaTypePick(unique: string | undefined) { this.value = { mediaTypeUnique: unique }; this._submitModal(); } render() { - return html` this.#onMediaTypePick()} label="Automatically" compact> - + return html`
+ this.#onMediaTypePick(undefined)} + label="Automatically" + compact> + Auto pick + ${repeat( this._options, (option) => option.unique, @@ -38,12 +49,30 @@ export class UmbDropzoneMediaTypePickerModalElement extends UmbModalBaseElement< @click=${() => this.#onMediaTypePick(option.unique)} label=${option.name} compact> - + ${option.name} `, - )}`; + )} +
`; } - static styles = [UmbTextStyles, css``]; + static styles = [ + UmbTextStyles, + css` + #options { + display: flex; + margin: var(--uui-size-layout-1); + gap: var(--uui-size-3); + } + uui-button { + width: 150px; + height: 150px; + } + umb-icon { + font-size: var(--uui-size-10); + margin-bottom: var(--uui-size-2); + } + `, + ]; } export default UmbDropzoneMediaTypePickerModalElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts index 4cd4a52974..e8437bf919 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts @@ -3,6 +3,7 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbDropzoneMediaTypePickerModalData { options: Array; + files?: Array; } export type UmbDropzoneMediaTypePickerModalValue = { From 376d08481f9cd4a20474b116e22ef378d1838947 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 1 May 2024 16:34:29 +0200 Subject: [PATCH 014/134] parentunique --- .../media/dropzone/dropzone-manager.class.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index f9caed9882..47254ee17e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -37,31 +37,20 @@ export class UmbDropzoneManager extends UmbControllerBase { #mediaDetailRepository = new UmbMediaDetailRepository(this); #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); - #parentUnique: string | null; - - constructor(host: UmbControllerHost, parentUnique: string | null) { + constructor(host: UmbControllerHost) { super(host); this.#host = host; - this.#parentUnique = parentUnique; } - public setParentUnique(parentUnique: string | null) { - this.#parentUnique = parentUnique; - } - public getParentUnique() { - return this.#parentUnique; - } - - public async dropOneFile(file: File) { + public async dropOneFile(file: File, parentUnique: string | null) { const extension = this.#getExtensionFromMimeType(file.type); if (!extension) { - // Folders have no extension on file drop. We assume it is a folder being uploaded. - this.#handleFolder(file); + // TODO Folders have no extension on file drop. Assume it is a folder being uploaded. return; } - const optionsArray = await this.#buildOptionsArrayFrom([extension]); + const optionsArray = await this.#buildOptionsArrayFrom([extension], parentUnique); if (!optionsArray.length) throw new Error('File not allowed here.'); // Parent does not allow this file type here. const mediaTypes = optionsArray[0].mediaTypes; @@ -89,11 +78,12 @@ export class UmbDropzoneManager extends UmbControllerBase { await this.#uploadOne(uploadableFile); } - public async dropFiles(files: Array) { + public async dropFiles(files: Array, parentUnique: string | null) { // removes duplicate file types so we don't call endpoints unnecessarily when building options. const mimeTypes = [...new Set(files.map((file) => file.type))]; const optionsArray = await this.#buildOptionsArrayFrom( mimeTypes.map((mimetype) => this.#getExtensionFromMimeType(mimetype)), + parentUnique, ); if (!optionsArray.length) return; // None of the files are allowed in current dropzone. @@ -105,7 +95,6 @@ export class UmbDropzoneManager extends UmbControllerBase { const extension = this.#getExtensionFromMimeType(file.type); if (!extension) { // Folders have no extension on file drop. We assume it is a folder being uploaded. - this.#handleFolder(file); return; } const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; @@ -125,9 +114,12 @@ export class UmbDropzoneManager extends UmbControllerBase { return getExtensionFromMime(mimeType) || ''; } - async #buildOptionsArrayFrom(fileExtensions: Array): Promise> { + async #buildOptionsArrayFrom( + fileExtensions: Array, + parentUnique: string | null, + ): Promise> { // Getting all media types allowed in our current position based on parent unique. - const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.#parentUnique); + const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(parentUnique); if (!allAllowedMediaTypes?.items.length) return []; const allowedByParent = allAllowedMediaTypes.items; @@ -160,10 +152,6 @@ export class UmbDropzoneManager extends UmbControllerBase { await this.#tempFileManager.uploadOne(file); } - async #handleFolder(file: File) { - throw new Error('Not implemented: Folders coming soon!'); - } - private _reset() { // } From 11af14852402481cdf31edbb5eab62b45475f6e7 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 2 May 2024 09:12:30 +0200 Subject: [PATCH 015/134] temporary file manager item --- .../temporary-file-manager.class.ts | 47 +++++++++++-------- .../media/dropzone/dropzone-manager.class.ts | 20 ++++---- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index 2f349c4bbf..a77778e47e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -4,7 +4,13 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; -export type TemporaryFileStatus = 'success' | 'waiting' | 'error'; +///export type TemporaryFileStatus = 'success' | 'waiting' | 'error'; + +export enum TemporaryFileStatus { + SUCCESS = 'success', + WAITING = 'waiting', + ERROR = 'error', +} export interface UmbTemporaryFileModel { file: File; @@ -16,10 +22,12 @@ export interface UmbTemporaryFileQueueModel extends Partial extends UmbControllerBase { #temporaryFileRepository; - #queue = new UmbArrayState([], (item) => item.unique); + #queue = new UmbArrayState([], (item) => item.unique); public readonly queue = this.#queue.asObservable(); constructor(host: UmbControllerHost) { @@ -27,28 +35,27 @@ export class UmbTemporaryFileManager extends UmbControllerBase { this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); } - async uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise> { + async uploadOne(uploadableItem: UploadableItem): Promise { this.#queue.setValue([]); - const item: UmbTemporaryFileModel = { - file: queueItem.file, - unique: queueItem.unique ?? UmbId.new(), - status: queueItem.status ?? 'waiting', + + const item: UploadableItem = { + unique: UmbId.new(), + status: TemporaryFileStatus.WAITING, + ...uploadableItem, }; + this.#queue.appendOne(item); - return this.handleQueue(); + return (await this.#handleQueue())[0]; } - async upload(queueItems: Array): Promise> { + async upload(queueItems: Array): Promise> { this.#queue.setValue([]); + const items = queueItems.map( - (item): UmbTemporaryFileModel => ({ - file: item.file, - unique: item.unique ?? UmbId.new(), - status: item.status ?? 'waiting', - }), + (item): UploadableItem => ({ unique: UmbId.new(), status: TemporaryFileStatus.WAITING, ...item }), ); this.#queue.append(items); - return this.handleQueue(); + return this.#handleQueue(); } removeOne(unique: string) { @@ -59,8 +66,8 @@ export class UmbTemporaryFileManager extends UmbControllerBase { this.#queue.remove(uniques); } - private async handleQueue() { - const filesCompleted: Array = []; + async #handleQueue() { + const filesCompleted: Array = []; const queue = this.#queue.getValue(); if (!queue.length) return filesCompleted; @@ -73,10 +80,10 @@ export class UmbTemporaryFileManager extends UmbControllerBase { let status: TemporaryFileStatus; if (error) { - status = 'error'; + status = TemporaryFileStatus.ERROR; this.#queue.updateOne(item.unique, { ...item, status }); } else { - status = 'success'; + status = TemporaryFileStatus.SUCCESS; this.#queue.updateOne(item.unique, { ...item, status }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 47254ee17e..477cf60c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -8,7 +8,7 @@ import { UmbMediaTypeDetailRepository, UmbMediaTypeStructureRepository, } from '@umbraco-cms/backoffice/media-type'; -import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; @@ -62,7 +62,7 @@ export class UmbDropzoneManager extends UmbControllerBase { mediaTypeUnique: mediaTypes[0].unique, }; - await this.#uploadOne(uploadableFile); + await this.#handleUpload([uploadableFile]); return; } @@ -75,7 +75,7 @@ export class UmbDropzoneManager extends UmbControllerBase { file, mediaTypeUnique: mediaType.unique, }; - await this.#uploadOne(uploadableFile); + await this.#handleUpload([uploadableFile]); } public async dropFiles(files: Array, parentUnique: string | null) { @@ -107,7 +107,7 @@ export class UmbDropzoneManager extends UmbControllerBase { uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); } - await this.#upload(uploadableFiles); + await this.#handleUpload(uploadableFiles); } #getExtensionFromMimeType(mimeType: string): string { @@ -144,12 +144,12 @@ export class UmbDropzoneManager extends UmbControllerBase { return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null; } - async #upload(files: Array) { - await this.#tempFileManager.upload(files); - } - - async #uploadOne(file: UmbUploadableFileModel) { - await this.#tempFileManager.uploadOne(file); + async #handleUpload(files: Array) { + const uploads = await this.#tempFileManager.upload(files); + for (const upload of uploads) { + if (upload.status !== TemporaryFileStatus.SUCCESS) return; + // TODO: Create item + } } private _reset() { From a37e828bbbff09e64247bdbcefcddf9156888c1b Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 2 May 2024 21:44:48 +0200 Subject: [PATCH 016/134] progressbar --- .../temporary-file-manager.class.ts | 12 +- .../media/dropzone/dropzone-manager.class.ts | 84 +++++-- .../media/media/dropzone/dropzone.element.ts | 219 ++---------------- 3 files changed, 85 insertions(+), 230 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index a77778e47e..c56077d8db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -14,16 +14,12 @@ export enum TemporaryFileStatus { export interface UmbTemporaryFileModel { file: File; - unique: string; - status: TemporaryFileStatus; -} - -export interface UmbTemporaryFileQueueModel extends Partial { - file: File; + unique?: string; + status?: TemporaryFileStatus; } export class UmbTemporaryFileManager< - UploadableItem extends UmbTemporaryFileQueueModel = UmbTemporaryFileQueueModel, + UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, > extends UmbControllerBase { #temporaryFileRepository; @@ -76,7 +72,7 @@ export class UmbTemporaryFileManager< if (!item.unique) throw new Error(`Unique is missing for item ${item}`); const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file); - await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown + //await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown let status: TemporaryFileStatus; if (error) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 477cf60c9c..1ddeaf2a4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -1,24 +1,26 @@ +import type { UmbMediaDetailModel } from '../types.js'; import { UmbMediaDetailRepository } from '../repository/index.js'; import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; import { mimeToExtension } from '@umbraco-cms/backoffice/external/mime-types'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; import { - type UmbAllowedMediaTypeModel, - UmbMediaTypeDetailRepository, - UmbMediaTypeStructureRepository, -} from '@umbraco-cms/backoffice/media-type'; -import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; + TemporaryFileStatus, + UmbTemporaryFileManager, + type UmbTemporaryFileModel, +} from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; -interface UmbUploadableFileModel { +export interface UmbUploadableFileModel extends UmbTemporaryFileModel { unique: string; file: File; mediaTypeUnique: string; } -export interface UmbUploadableFileExtensionModel { +export interface UmbUploadableExtensionModel { fileExtension: string; mediaTypes: Array; } @@ -31,18 +33,35 @@ export function getExtensionFromMime(mimeType: string): string | undefined { export class UmbDropzoneManager extends UmbControllerBase { #host; + #tempFileManager = new UmbTemporaryFileManager(this); #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); #mediaDetailRepository = new UmbMediaDetailRepository(this); - #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); + + #progress = new UmbNumberState(undefined); + public readonly progress = this.#progress.asObservable(); constructor(host: UmbControllerHost) { super(host); this.#host = host; + + this.observe( + this.#tempFileManager.queue, + (queue) => { + // TODO Reconsider how the progress bar should be handled. Here we are just showing the progress of the queue, rather than the progress of the files getting created as a media item. + // Maybe we want to create the media item right away after the corresponding file is uploaded as temp file and set the progress, then continue on to the next file... + if (!queue.length) return; + const waiting = queue.filter((item) => item.status === TemporaryFileStatus.WAITING); + const progress = waiting.length ? waiting.length / queue.length : 0; + this.#progress.setValue(progress); + }, + '_observeQueue', + ); } public async dropOneFile(file: File, parentUnique: string | null) { + this.#progress.setValue(0); const extension = this.#getExtensionFromMimeType(file.type); if (!extension) { @@ -62,7 +81,7 @@ export class UmbDropzoneManager extends UmbControllerBase { mediaTypeUnique: mediaTypes[0].unique, }; - await this.#handleUpload([uploadableFile]); + await this.#handleUpload([uploadableFile], parentUnique); return; } @@ -75,10 +94,11 @@ export class UmbDropzoneManager extends UmbControllerBase { file, mediaTypeUnique: mediaType.unique, }; - await this.#handleUpload([uploadableFile]); + await this.#handleUpload([uploadableFile], parentUnique); } public async dropFiles(files: Array, parentUnique: string | null) { + this.#progress.setValue(0); // removes duplicate file types so we don't call endpoints unnecessarily when building options. const mimeTypes = [...new Set(files.map((file) => file.type))]; const optionsArray = await this.#buildOptionsArrayFrom( @@ -107,7 +127,7 @@ export class UmbDropzoneManager extends UmbControllerBase { uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); } - await this.#handleUpload(uploadableFiles); + await this.#handleUpload(uploadableFiles, parentUnique); } #getExtensionFromMimeType(mimeType: string): string { @@ -117,7 +137,7 @@ export class UmbDropzoneManager extends UmbControllerBase { async #buildOptionsArrayFrom( fileExtensions: Array, parentUnique: string | null, - ): Promise> { + ): Promise> { // Getting all media types allowed in our current position based on parent unique. const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(parentUnique); if (!allAllowedMediaTypes?.items.length) return []; @@ -125,7 +145,7 @@ export class UmbDropzoneManager extends UmbControllerBase { const allowedByParent = allAllowedMediaTypes.items; // Building an array of options the files can be uploaded as. - const options: Array = []; + const options: Array = []; for (const fileExtension of fileExtensions) { const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); @@ -144,11 +164,39 @@ export class UmbDropzoneManager extends UmbControllerBase { return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null; } - async #handleUpload(files: Array) { - const uploads = await this.#tempFileManager.upload(files); + async #handleUpload(files: Array, parentUnique: string | null) { + const uploads = (await this.#tempFileManager.upload(files)) as Array; for (const upload of uploads) { - if (upload.status !== TemporaryFileStatus.SUCCESS) return; - // TODO: Create item + if (upload.status !== TemporaryFileStatus.SUCCESS) return; // Upload failed. In what way do we let the user know? + const preset: Partial = { + mediaType: { + unique: upload.mediaTypeUnique, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: upload.file.name, + createDate: null, + updateDate: null, + }, + ], + values: [ + { + alias: 'umbracoFile', + //value: { temporaryFileId: upload.unique }, + value: { src: upload.unique }, + culture: null, + segment: null, + }, + ], + }; + + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + if (!data) return; + + await this.#mediaDetailRepository.create(data, parentUnique); } } @@ -157,6 +205,8 @@ export class UmbDropzoneManager extends UmbControllerBase { } public destroy() { + this.#tempFileManager.destroy(); + this.#progress.destroy(); super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 96f6e638a0..7a971e3457 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -1,73 +1,19 @@ -import { UmbMediaDetailRepository } from '../repository/index.js'; -import type { UmbMediaDetailModel } from '../types.js'; import { UmbDropzoneManager } from './dropzone-manager.class.js'; -import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { - type UmbAllowedMediaTypeModel, - UmbMediaTypeStructureRepository, - UmbMediaTypeDetailRepository, -} from '@umbraco-cms/backoffice/media-type'; -import { - UmbTemporaryFileManager, - type UmbTemporaryFileQueueModel, - type UmbTemporaryFileModel, -} from '@umbraco-cms/backoffice/temporary-file'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; - -interface MediaTypeOptions { - fileExtension: string; - mediaTypes: Array; -} - -interface UploadableFile { - file: File; - mediaType: UmbAllowedMediaTypeModel; - regularUploadField?: boolean; - temporaryUnique?: string; -} @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { - #fileManager = new UmbTemporaryFileManager(this); - #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); - #mediaDetailRepository = new UmbMediaDetailRepository(this); - #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); - - #parentUnique: string | null = null; - #dropzoneManager = new UmbDropzoneManager(this, null); - - #allowedMediaTypes: Array = []; - - @state() - private queue: Array = []; - @property({ attribute: false }) - public set parentUnique(value: string | null) { - this.#parentUnique = value; - //this.#dropzoneManager.setParentUnique(value); - } - public get parentUnique(): string | null { - return this.#parentUnique; - } + parentUnique: string | null = null; public browse() { const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; return element.browse(); } - async #getAllowedMediaTypes(): Promise { - const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.parentUnique); - return data?.items ?? []; - } - - async #getAllowedMediaTypesOf(fileExtension: string): Promise { - const options = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); - const mediaTypes = options.filter((option) => this.#allowedMediaTypes.includes(option)); - return { fileExtension, mediaTypes }; - } - constructor() { super(); document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); @@ -101,159 +47,22 @@ export class UmbDropzoneElement extends UmbLitElement { const files: Array = event.detail.files; if (!files.length) return; - if (files.length === 1) { - this.#dropzoneManager.dropOneFile(files[0]); - } else { - this.#dropzoneManager.dropFiles(files); - } - } - /* - async #handleOneFile(file: File) { - const extension = getExtensionFromMime(file.type); - if (!extension) return; // Extension doesn't exist. - - const options = await this.#getAllowedMediaTypesOf(extension); - if (!options.mediaTypes.length) return; // File type not allowed in current dropzone. - - if (options.mediaTypes.length === 1) { - this.#uploadFiles([{ file, mediaType: options.mediaTypes[0] }]); - return; - } - - // Multiple options, show a dialog to the user to pick one. - //TODO: Implement dialog. - } - - async #handleMultipleFiles(files: Array) { - // removes duplicate file types so we don't call the endpoint unnecessarily for every file. - const types = [...new Set(files.map((file) => file.type))]; - const options: Array = []; - - for (const type of types) { - const extension = getExtensionFromMime(type); - if (!extension) return; // Extension doesn't exist. - - options.push(await this.#getAllowedMediaTypesOf(extension)); - } - - // We are just going to automatically pick the first possible media type option for now, but consider an option dialog in the future. - const uploadable: Array = []; - files.forEach((file) => { - const mediaType = options.find((option) => option.fileExtension === file.type)?.mediaTypes[0] ?? undefined; - if (mediaType) uploadable.push({ file, mediaType }); - }); - - this.#uploadFiles(uploadable); - } - - async #uploadFiles(uploadable: Array) { - const queue = uploadable.map( - (item): UmbTemporaryFileQueueModel => ({ file: item.file, unique: item.temporaryUnique }), + const dropzoneManager = new UmbDropzoneManager(this); + this.observe( + dropzoneManager.progress, + (progress) => { + if (progress === undefined) return; + this.dispatchEvent(new UmbProgressEvent(progress)); + }, + '_observeProgress', ); - const uploaded = await this.#fileManager.upload(queue); + await dropzoneManager.dropFiles(files, this.parentUnique); - for (const upload of uploaded) { - const mediaType = uploadable.find((item) => item.temporaryUnique === upload.unique)?.mediaType; - const value = mediaType?.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; - - if (!mediaType) return; - - const preset: Partial = { - mediaType: { - unique: mediaType.unique, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: upload.file.type - ? [ - { - alias: 'umbracoFile', - value, - culture: null, - segment: null, - }, - ] - : [], - }; - - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (!data) return; - - await this.#mediaDetailRepository.create(data, this.parentUnique); - - this.dispatchEvent(new UmbChangeEvent()); - } + this.dispatchEvent(new UmbChangeEvent()); // complete + dropzoneManager.destroy(); } - async #uploadHandler(files: Array) { - //TODO: Folders uploaded via UUIDropzone are always empty. Investigate why. - - const folders = files.filter((item) => !item.type).map((file): UmbTemporaryFileQueueModel => ({ file })); - const mediaItems = files.filter((item) => item.type); - - const queue = mediaItems.map((file): UmbTemporaryFileQueueModel => ({ file })); - - const uploaded = await this.#fileManager.upload(queue); - return [...folders, ...uploaded]; - } - - - async #onFileUpload(event: UUIFileDropzoneEvent) { - const files: Array = event.detail.files; - - if (!files.length) return; - const uploads = await this.#uploadHandler(files); - - for (const upload of uploads) { - const mediaType = this.#getMediaTypeFromMime(upload.file.type); - const value = mediaType.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique; - - const preset: Partial = { - mediaType: { - unique: mediaType.unique, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: upload.file.type - ? [ - { - alias: 'umbracoFile', - value, - culture: null, - segment: null, - }, - ] - : [], - }; - - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (!data) return; - - await this.#mediaDetailRepository.create(data, this.parentUnique); - - this.dispatchEvent(new UmbChangeEvent()); - } - } - - */ - render() { return html` Date: Fri, 3 May 2024 10:50:17 +0200 Subject: [PATCH 017/134] progressbar, delete old element, comments --- .../dropzone-media/dropzone-media.element.ts | 172 ------------------ .../media/components/dropzone-media/index.ts | 1 - .../packages/media/media/components/index.ts | 1 - .../media/dropzone/dropzone-manager.class.ts | 146 +++++++-------- .../src/packages/media/media/utils/index.ts | 8 + 5 files changed, 83 insertions(+), 245 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts deleted file mode 100644 index 12e32b5490..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { UmbMediaDetailRepository } from '../../repository/index.js'; -import type { UmbMediaDetailModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; -import { - UmbTemporaryFileManager, - type UmbTemporaryFileQueueModel, - type UmbTemporaryFileModel, -} from '@umbraco-cms/backoffice/temporary-file'; -import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; - -@customElement('umb-dropzone-media') -export class UmbDropzoneMediaElement extends UmbLitElement { - #fileManager = new UmbTemporaryFileManager(this); - #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); - #allowedMediaTypes: Array = []; - #mediaDetailRepository = new UmbMediaDetailRepository(this); - - @state() - private queue: Array = []; - - constructor() { - super(); - - this.observe(this.#fileManager.queue, (queue) => { - this.queue = queue; - const completed = queue.filter((item) => item.status !== 'waiting'); - const progress = Math.round((completed.length / queue.length) * 100); - this.dispatchEvent(new UmbProgressEvent(progress)); - }); - - this.#getAllowedMediaTypes(); - document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); - document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); - document.addEventListener('drop', this.#handleDrop.bind(this)); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - document.removeEventListener('dragenter', this.#handleDragEnter.bind(this)); - document.removeEventListener('dragleave', this.#handleDragLeave.bind(this)); - document.removeEventListener('drop', this.#handleDrop.bind(this)); - } - - #handleDragEnter() { - this.toggleAttribute('dragging', true); - } - - #handleDragLeave() { - this.toggleAttribute('dragging', false); - } - - #handleDrop(event: DragEvent) { - event.preventDefault(); - this.toggleAttribute('dragging', false); - } - - async #getAllowedMediaTypes() { - const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(null); - if (!data) return; - this.#allowedMediaTypes = data.items; - } - /* - #getMediaTypeFromMime(mimetype: string): UmbAllowedMediaTypeModel | undefined { - //const mediaTypeName = getMediaTypeByFileMimeType(mimetype); - //return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; - } - */ - - async #uploadHandler(files: Array) { - const queue = files.map((file): UmbTemporaryFileQueueModel => ({ file })); - const uploaded = await this.#fileManager.upload(queue); - return uploaded; - } - - async #onFileUpload(event: UUIFileDropzoneEvent) { - const files: Array = event.detail.files; - if (!files.length) return; - const uploads = await this.#uploadHandler(files); - - for (const upload of uploads) { - //const mediaType = this.#getMediaTypeFromMime(upload.file.type); - - const preset: Partial = { - mediaType: { - unique: /*mediaType.unique*/ '123', - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: [ - { - alias: 'umbracoFile', - value: { src: upload.unique }, - culture: null, - segment: null, - }, - ], - }; - - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (!data) return; - - await this.#mediaDetailRepository.create(data, null); - - this.dispatchEvent(new UmbChangeEvent()); - } - } - - render() { - return html``; - } - - static styles = [ - css` - :host([dragging]) #dropzone { - opacity: 1; - pointer-events: all; - } - - [dropzone] { - opacity: 0; - } - - #dropzone { - opacity: 0; - pointer-events: none; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - inset: 0px; - z-index: 100; - backdrop-filter: opacity(1); /* Removes the built in blur effect */ - border-radius: var(--uui-border-radius); - overflow: clip; - border: 1px solid var(--uui-color-focus); - } - #dropzone:after { - content: ''; - display: block; - position: absolute; - inset: 0; - border-radius: var(--uui-border-radius); - background-color: var(--uui-color-focus); - opacity: 0.2; - } - `, - ]; -} - -export default UmbDropzoneMediaElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-dropzone-media': UmbDropzoneMediaElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts deleted file mode 100644 index a6c28ed09c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dropzone-media.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts index d884d15b41..3c48dc1e29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts @@ -1,5 +1,4 @@ import './input-media/index.js'; -export * from './dropzone-media/index.js'; export * from './input-media/index.js'; export * from './input-image-cropper/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 1ddeaf2a4a..41161dedfb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -1,7 +1,7 @@ +import { getExtensionFromMime } from '../utils/index.js'; import type { UmbMediaDetailModel } from '../types.js'; import { UmbMediaDetailRepository } from '../repository/index.js'; import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; -import { mimeToExtension } from '@umbraco-cms/backoffice/external/mime-types'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; @@ -25,12 +25,6 @@ export interface UmbUploadableExtensionModel { mediaTypes: Array; } -export function getExtensionFromMime(mimeType: string): string | undefined { - const extension = mimeToExtension(mimeType); - if (!extension) return; // extension doesn't exist. - return extension; -} - export class UmbDropzoneManager extends UmbControllerBase { #host; @@ -60,7 +54,45 @@ export class UmbDropzoneManager extends UmbControllerBase { ); } - public async dropOneFile(file: File, parentUnique: string | null) { + public async dropFiles(files: Array, parentUnique: string | null) { + if (!files.length) return; + if (files.length === 1) return this.#handleOneOneFile(files[0], parentUnique); + + // Handler for multiple files dropped + + this.#progress.setValue(0); + // removes duplicate file types so we don't call endpoints unnecessarily when building options. + const mimeTypes = [...new Set(files.map((file) => file.type))]; + const optionsArray = await this.#buildOptionsArrayFrom( + mimeTypes.map((mimetype) => this.#getExtensionFromMimeType(mimetype)), + parentUnique, + ); + + if (!optionsArray.length) return; // None of the files are allowed in current dropzone. + + // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones? + const uploadableFiles: Array = []; + + for (const file of files) { + const extension = this.#getExtensionFromMimeType(file.type); + if (!extension) { + // Folders have no extension on file drop. We assume it is a folder being uploaded. + return; + } + const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; + + if (!options) return; // TODO Current dropped file not allowed in this area. Find a good way to show this to the user after we finish uploading the rest of the files. + + // Since we are uploading multiple files, we will pick first allowed option. + // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? + const mediaType = options[0]; + uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); + } + + await this.#handleUpload(uploadableFiles, parentUnique); + } + + async #handleOneOneFile(file: File, parentUnique: string | null) { this.#progress.setValue(0); const extension = this.#getExtensionFromMimeType(file.type); @@ -97,39 +129,6 @@ export class UmbDropzoneManager extends UmbControllerBase { await this.#handleUpload([uploadableFile], parentUnique); } - public async dropFiles(files: Array, parentUnique: string | null) { - this.#progress.setValue(0); - // removes duplicate file types so we don't call endpoints unnecessarily when building options. - const mimeTypes = [...new Set(files.map((file) => file.type))]; - const optionsArray = await this.#buildOptionsArrayFrom( - mimeTypes.map((mimetype) => this.#getExtensionFromMimeType(mimetype)), - parentUnique, - ); - - if (!optionsArray.length) return; // None of the files are allowed in current dropzone. - - // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones? - const uploadableFiles: Array = []; - - for (const file of files) { - const extension = this.#getExtensionFromMimeType(file.type); - if (!extension) { - // Folders have no extension on file drop. We assume it is a folder being uploaded. - return; - } - const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; - - if (!options) return; // Dropped file not allowed in current dropzone. - - // Since we are uploading multiple files, we will pick first allowed option. - // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? - const mediaType = options[0]; - uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); - } - - await this.#handleUpload(uploadableFiles, parentUnique); - } - #getExtensionFromMimeType(mimeType: string): string { return getExtensionFromMime(mimeType) || ''; } @@ -165,38 +164,43 @@ export class UmbDropzoneManager extends UmbControllerBase { } async #handleUpload(files: Array, parentUnique: string | null) { - const uploads = (await this.#tempFileManager.upload(files)) as Array; - for (const upload of uploads) { - if (upload.status !== TemporaryFileStatus.SUCCESS) return; // Upload failed. In what way do we let the user know? - const preset: Partial = { - mediaType: { - unique: upload.mediaTypeUnique, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: [ - { - alias: 'umbracoFile', - //value: { temporaryFileId: upload.unique }, - value: { src: upload.unique }, - culture: null, - segment: null, - }, - ], - }; + let index: number = 0; + for (const file of files) { + index++; + const upload = (await this.#tempFileManager.uploadOne(file)) as UmbUploadableFileModel; - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (!data) return; + if (upload.status === TemporaryFileStatus.SUCCESS) { + // Upload successful. Create media item. + const preset: Partial = { + mediaType: { + unique: upload.mediaTypeUnique, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: upload.file.name, + createDate: null, + updateDate: null, + }, + ], + values: [ + { + alias: 'umbracoFile', + value: { temporaryFileId: upload.unique }, + culture: null, + segment: null, + }, + ], + }; + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + await this.#mediaDetailRepository.create(data!, parentUnique); + } + // TODO Find a good way to show files that ended up as TemporaryFileStatus.ERROR. Notice that they were allowed in current area - await this.#mediaDetailRepository.create(data, parentUnique); + const progress = Math.floor((index / files.length) * 100); + this.#progress.setValue(progress); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts index 49550cabaa..23f7e0f5c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts @@ -4,6 +4,7 @@ import { getProcessedImageUrl } from '@umbraco-cms/backoffice/utils'; import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; +import { mimeToExtension } from '@umbraco-cms/backoffice/external/mime-types'; /** * Sizes an image in the editor @@ -122,3 +123,10 @@ export async function uploadBlobImages(editor: Editor, newContent?: string) { }); } } + +/** Converts the mime type into the corresponding file extension. Returns null if can't convert. */ +export function getExtensionFromMime(mimeType: string): string | null { + const extension = mimeToExtension(mimeType); + if (!extension) return null; // extension doesn't exist. + return extension; +} From 0506666333fc9b274b99dfa4008447753b8e298d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 11:08:01 +0200 Subject: [PATCH 018/134] delete leftovers, progress update --- .../media/dropzone/dropzone-manager.class.ts | 28 ++------ .../media/media/dropzone/dropzone.element.ts | 9 +-- ...oadable-structure-data-source.interface.ts | 16 ----- ...pe-uploadable-structure-repository-base.ts | 32 --------- ...loadable-structure-repository.interface.ts | 7 -- ...dable-structure-server-data-source-base.ts | 66 ------------------- 6 files changed, 12 insertions(+), 146 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 41161dedfb..b95437bc2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -12,7 +12,7 @@ import { } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export interface UmbUploadableFileModel extends UmbTemporaryFileModel { unique: string; @@ -33,25 +33,12 @@ export class UmbDropzoneManager extends UmbControllerBase { #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); #mediaDetailRepository = new UmbMediaDetailRepository(this); - #progress = new UmbNumberState(undefined); - public readonly progress = this.#progress.asObservable(); + #completed = new UmbArrayState([], (upload) => upload.unique); + public readonly completed = this.#completed.asObservable(); constructor(host: UmbControllerHost) { super(host); this.#host = host; - - this.observe( - this.#tempFileManager.queue, - (queue) => { - // TODO Reconsider how the progress bar should be handled. Here we are just showing the progress of the queue, rather than the progress of the files getting created as a media item. - // Maybe we want to create the media item right away after the corresponding file is uploaded as temp file and set the progress, then continue on to the next file... - if (!queue.length) return; - const waiting = queue.filter((item) => item.status === TemporaryFileStatus.WAITING); - const progress = waiting.length ? waiting.length / queue.length : 0; - this.#progress.setValue(progress); - }, - '_observeQueue', - ); } public async dropFiles(files: Array, parentUnique: string | null) { @@ -60,7 +47,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // Handler for multiple files dropped - this.#progress.setValue(0); + this.#completed.setValue([]); // removes duplicate file types so we don't call endpoints unnecessarily when building options. const mimeTypes = [...new Set(files.map((file) => file.type))]; const optionsArray = await this.#buildOptionsArrayFrom( @@ -93,7 +80,7 @@ export class UmbDropzoneManager extends UmbControllerBase { } async #handleOneOneFile(file: File, parentUnique: string | null) { - this.#progress.setValue(0); + this.#completed.setValue([]); const extension = this.#getExtensionFromMimeType(file.type); if (!extension) { @@ -199,8 +186,7 @@ export class UmbDropzoneManager extends UmbControllerBase { } // TODO Find a good way to show files that ended up as TemporaryFileStatus.ERROR. Notice that they were allowed in current area - const progress = Math.floor((index / files.length) * 100); - this.#progress.setValue(progress); + this.#completed.setValue([...this.#completed.getValue(), upload]); } } @@ -210,7 +196,7 @@ export class UmbDropzoneManager extends UmbControllerBase { public destroy() { this.#tempFileManager.destroy(); - this.#progress.destroy(); + this.#completed.destroy(); super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 7a971e3457..712a07f585 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -49,12 +49,13 @@ export class UmbDropzoneElement extends UmbLitElement { const dropzoneManager = new UmbDropzoneManager(this); this.observe( - dropzoneManager.progress, - (progress) => { - if (progress === undefined) return; + dropzoneManager.completed, + (completed) => { + if (!completed.length) return; + const progress = Math.floor(completed.length / files.length); this.dispatchEvent(new UmbProgressEvent(progress)); }, - '_observeProgress', + '_observeCompleted', ); await dropzoneManager.dropFiles(files, this.parentUnique); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts deleted file mode 100644 index 35422dfc25..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-data-source.interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { - UmbContentTypeStructureDataSource, - UmbContentTypeStructureDataSourceConstructor, -} from '@umbraco-cms/backoffice/content-type'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -export interface UmbContentTypeUploadableStructureDataSourceConstructor - extends UmbContentTypeStructureDataSourceConstructor { - new (host: UmbControllerHost): UmbContentTypeUploadableStructureDataSource; -} - -export interface UmbContentTypeUploadableStructureDataSource - extends UmbContentTypeStructureDataSource { - getAllowedMediaTypesOf(fileExtension: string | null): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts deleted file mode 100644 index 60805f94cf..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository-base.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - UmbContentTypeUploadableStructureDataSource, - UmbContentTypeUploadableStructureDataSourceConstructor, -} from './content-type-uploadable-structure-data-source.interface.js'; -import type { UmbContentTypeUploadableStructureRepository } from './content-type-uploadable-structure-repository.interface.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type'; - -export abstract class UmbContentTypeUploadableStructureRepositoryBase - extends UmbContentTypeStructureRepositoryBase - implements UmbContentTypeUploadableStructureRepository -{ - #structureSource: UmbContentTypeUploadableStructureDataSource; - - constructor( - host: UmbControllerHost, - structureSource: UmbContentTypeUploadableStructureDataSourceConstructor, - ) { - super(host, structureSource); - this.#structureSource = new structureSource(host); - } - - /** - * Returns a promise with the allowed media-types of a uploadable content type. - * @param {string} unique - * @return {*} - * @memberof UmbContentTypeUploadableStructureRepositoryBase - */ - requestAllowedMediaTypesOf(fileExtension: string | null) { - return this.#structureSource.getAllowedMediaTypesOf(fileExtension); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts deleted file mode 100644 index b25083e1ae..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-repository.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UmbContentTypeStructureRepository } from '@umbraco-cms/backoffice/content-type'; -import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -export interface UmbContentTypeUploadableStructureRepository - extends UmbContentTypeStructureRepository { - requestAllowedMediaTypesOf(unique: string): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts deleted file mode 100644 index 31e110ee31..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/repository/content-type-uploadable-structure-server-data-source-base.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - type UmbContentTypeStructureDataSource, - UmbContentTypeStructureServerDataSourceBase, - type UmbContentTypeStructureServerDataSourceBaseArgs, -} from '@umbraco-cms/backoffice/content-type'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; - -// Temp type -type AllowedContentTypeModel = { - id: string; - name: string; - description?: string | null; - icon?: string | null; -}; - -export interface UmbContentTypeUploadableStructureServerDataSourceBaseArgs< - ServerItemType extends AllowedContentTypeModel, - ClientItemType extends { unique: string }, -> extends UmbContentTypeStructureServerDataSourceBaseArgs { - getAllowedMediaTypesOf: (fileExtension: string | null) => Promise; -} - -export abstract class UmbContentTypeUploadableStructureServerDataSourceBase< - ServerItemType extends AllowedContentTypeModel, - ClientItemType extends { unique: string }, - > - extends UmbContentTypeStructureServerDataSourceBase - implements UmbContentTypeStructureDataSource -{ - #host; - #getAllowedChildrenOf; - #mapper; - - /** - * Creates an instance of UmbContentTypeStructureServerDataSourceBase. - * @param {UmbControllerHost} host - * @memberof UmbItemServerDataSourceBase - */ - constructor( - host: UmbControllerHost, - args: UmbContentTypeUploadableStructureServerDataSourceBaseArgs, - ) { - super(host, args); - this.#host = host; - this.#getAllowedChildrenOf = args.getAllowedChildrenOf; - this.#mapper = args.mapper; - } - - /** - * Returns a promise with the allowed content types for the given unique - * @param {string} unique - * @return {*} - * @memberof UmbContentTypeStructureServerDataSourceBase - */ - async getAllowedMediaTypesOf(fileExtension: string | null) { - const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAllowedChildrenOf(fileExtension)); - - if (data) { - const items = data.items.map((item) => this.#mapper(item)); - return { data: { items, total: data.total } }; - } - - return { error }; - } -} From 65fe32656d806509bd1fd825920706e5b2b0c36d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 11:13:12 +0200 Subject: [PATCH 019/134] temp file model --- .../input-upload-field/input-upload-field.element.ts | 3 +-- .../core/temporary-file/temporary-file-manager.class.ts | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index aabd23654f..8e3dd01e69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -147,12 +147,11 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen (file): UmbTemporaryFileModel => ({ unique: UmbId.new(), file, - status: 'waiting', }), ); this.#manager.upload(items); - this.keys = items.map((item) => item.unique); + this.keys = items.map((item) => item.unique!); this.value = this.keys.join(','); this.dispatchEvent(new UmbChangeEvent()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index c56077d8db..0f4921433d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -14,7 +14,7 @@ export enum TemporaryFileStatus { export interface UmbTemporaryFileModel { file: File; - unique?: string; + unique: string; status?: TemporaryFileStatus; } @@ -35,7 +35,6 @@ export class UmbTemporaryFileManager< this.#queue.setValue([]); const item: UploadableItem = { - unique: UmbId.new(), status: TemporaryFileStatus.WAITING, ...uploadableItem, }; @@ -47,9 +46,7 @@ export class UmbTemporaryFileManager< async upload(queueItems: Array): Promise> { this.#queue.setValue([]); - const items = queueItems.map( - (item): UploadableItem => ({ unique: UmbId.new(), status: TemporaryFileStatus.WAITING, ...item }), - ); + const items = queueItems.map((item): UploadableItem => ({ status: TemporaryFileStatus.WAITING, ...item })); this.#queue.append(items); return this.#handleQueue(); } From 7e3cf2adbf553192b0aa922d7be514cd500cba63 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 12:07:27 +0200 Subject: [PATCH 020/134] mime --- src/Umbraco.Web.UI.Client/package-lock.json | 30 ++++++++++++++----- src/Umbraco.Web.UI.Client/package.json | 4 +-- .../src/external/mime-types/index.ts | 1 - .../src/external/mime/index.ts | 2 ++ .../src/packages/media/media/utils/index.ts | 4 +-- src/Umbraco.Web.UI.Client/tsconfig.json | 2 +- 6 files changed, 30 insertions(+), 13 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/external/mime/index.ts diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 6dcbcaa524..1ee7114fea 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -20,7 +20,7 @@ "element-internals-polyfill": "^1.3.10", "lit": "^3.1.2", "marked": "^12.0.0", - "mime-types": "^2.1.35", + "mime": "^4.0.3", "monaco-editor": "^0.46.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", @@ -15835,21 +15835,24 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", + "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -15858,6 +15861,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -18414,6 +18418,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 145048918e..fbb90aafbc 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -97,7 +97,7 @@ "./external/dompurify": "./dist-cms/external/dompurify/index.js", "./external/lit": "./dist-cms/external/lit/index.js", "./external/marked": "./dist-cms/external/marked/index.js", - "./external/mime-types": "./dist-cms/external/mime-types/index.js", + "./external/mime": "./dist-cms/external/mime/index.js", "./external/monaco-editor": "./dist-cms/external/monaco-editor/index.js", "./external/openid": "./dist-cms/external/openid/index.js", "./external/router-slot": "./dist-cms/external/router-slot/index.js", @@ -181,7 +181,7 @@ "element-internals-polyfill": "^1.3.10", "lit": "^3.1.2", "marked": "^12.0.0", - "mime-types": "^2.1.35", + "mime": "^4.0.3", "monaco-editor": "^0.46.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", diff --git a/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts b/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts deleted file mode 100644 index 44b10e99aa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/external/mime-types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { extension as mimeToExtension } from 'mime-types'; diff --git a/src/Umbraco.Web.UI.Client/src/external/mime/index.ts b/src/Umbraco.Web.UI.Client/src/external/mime/index.ts new file mode 100644 index 0000000000..6263371533 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/mime/index.ts @@ -0,0 +1,2 @@ +import mime from 'mime'; +export { mime }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts index 23f7e0f5c4..4d59e5ba5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/utils/index.ts @@ -4,7 +4,7 @@ import { getProcessedImageUrl } from '@umbraco-cms/backoffice/utils'; import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; -import { mimeToExtension } from '@umbraco-cms/backoffice/external/mime-types'; +import { mime } from '@umbraco-cms/backoffice/external/mime'; /** * Sizes an image in the editor @@ -126,7 +126,7 @@ export async function uploadBlobImages(editor: Editor, newContent?: string) { /** Converts the mime type into the corresponding file extension. Returns null if can't convert. */ export function getExtensionFromMime(mimeType: string): string | null { - const extension = mimeToExtension(mimeType); + const extension = mime.getExtension(mimeType); if (!extension) return null; // extension doesn't exist. return extension; } diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index dd94a1b69c..5a45799abf 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -115,7 +115,7 @@ "@umbraco-cms/backoffice/external/dompurify": ["./src/external/dompurify/index.ts"], "@umbraco-cms/backoffice/external/lit": ["./src/external/lit/index.ts"], "@umbraco-cms/backoffice/external/marked": ["./src/external/marked/index.ts"], - "@umbraco-cms/backoffice/external/mime-types": ["./src/external/mime-types/index.ts"], + "@umbraco-cms/backoffice/external/mime": ["src/external/mime/index.ts"], "@umbraco-cms/backoffice/external/monaco-editor": ["./src/external/monaco-editor/index.ts"], "@umbraco-cms/backoffice/external/openid": ["./src/external/openid/index.ts"], "@umbraco-cms/backoffice/external/router-slot": ["./src/external/router-slot/index.ts"], From 76967ff190a39a09d614be795d80c97b8fb3a6cc Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 12:13:13 +0200 Subject: [PATCH 021/134] observe --- .../packages/media/media/dropzone/dropzone.element.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 712a07f585..78196e5b3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -52,16 +52,19 @@ export class UmbDropzoneElement extends UmbLitElement { dropzoneManager.completed, (completed) => { if (!completed.length) return; + const progress = Math.floor(completed.length / files.length); this.dispatchEvent(new UmbProgressEvent(progress)); + + if (completed.length === files.length) { + this.dispatchEvent(new UmbChangeEvent()); + dropzoneManager.destroy(); + } }, '_observeCompleted', ); - + //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. await dropzoneManager.dropFiles(files, this.parentUnique); - - this.dispatchEvent(new UmbChangeEvent()); // complete - dropzoneManager.destroy(); } render() { From c12c917fa91b2e1dfea988e401bc184cce011ddb Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 14:58:12 +0200 Subject: [PATCH 022/134] align uploadfield property with endpoint --- .../input-upload-field-file.element.ts | 3 +- .../input-upload-field.element.ts | 233 +++++------------- ...property-editor-ui-upload-field.element.ts | 33 ++- 3 files changed, 71 insertions(+), 198 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts index 180606cbad..ecb8612d3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts @@ -23,14 +23,13 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement { label = ''; #serverUrl = ''; - #serverUrlPromise; /** * */ constructor() { super(); - this.#serverUrlPromise = this.consumeContext(UMB_APP_CONTEXT, (instance) => { + this.consumeContext(UMB_APP_CONTEXT, (instance) => { this.#serverUrl = instance.getServerUrl(); }).asPromise(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index 8e3dd01e69..6dc715bb03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -1,5 +1,6 @@ +import type { MediaValueType } from '../../../property-editors/upload-field/property-editor-ui-upload-field.element.js'; import type { UmbTemporaryFileModel } from '../../temporary-file/temporary-file-manager.class.js'; -import { UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js'; +import { TemporaryFileStatus, UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { css, @@ -10,36 +11,26 @@ import { property, query, state, - repeat, } from '@umbraco-cms/backoffice/external/lit'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -import './input-upload-field-file.element.js'; +import { mime } from '@umbraco-cms/backoffice/external/mime'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; +import './input-upload-field-file.element.js'; @customElement('umb-input-upload-field') -export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElement, '') { - private _keys: Array = []; - /** - * @description Keys to the files that belong to this upload field. - * @type {Array} - * @default [] - */ - @property({ type: Array }) - public set keys(fileKeys: Array) { - this._keys = fileKeys; - super.value = this._keys.join(','); - this.#setFilePaths(); +export class UmbInputUploadFieldElement extends UmbLitElement { + @property({ type: Object }) + set value(value: MediaValueType) { + if (!value?.src) return; + this._src = value.src; } - public get keys(): Array { - return this._keys; + get value(): MediaValueType { + return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique }; } /** - * @description Allowed file extensions. If left empty, all are allowed. + * @description Allowed file extensions. Allow all if empty. * @type {Array} * @default undefined */ @@ -51,21 +42,11 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen return this._extensions; } - /** - * @description Allows the user to upload multiple files. - * @default false - * @attr - */ - @property({ type: Boolean }) - public multiple = false; + @state() + public temporaryFile?: UmbTemporaryFileModel; @state() - private _files: Array<{ - path: string; - unique: string; - queueItem?: UmbTemporaryFileModel; - file?: File; - }> = []; + private _src = ''; @state() private _extensions?: string[]; @@ -73,109 +54,36 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen @query('#dropzone') private _dropzone?: UUIFileDropzoneElement; - #manager; - #serverUrl = ''; - #serverUrlPromise; + #manager = new UmbTemporaryFileManager(this); - protected getFormElement() { - return undefined; - } - - constructor() { - super(); - this.#manager = new UmbTemporaryFileManager(this); - - /*this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => { - this.observe(await context.propertyValueByAlias('umbracoExtension'), (value) => { - //const test = value; - }); - });*/ - - this.#serverUrlPromise = this.consumeContext(UMB_APP_CONTEXT, (instance) => { - this.#serverUrl = instance.getServerUrl(); - }).asPromise(); - - this.observe(this.#manager.queue, (value) => { - this.error = !value.length; - this._files = this._files.map((file) => { - const queueItem = value.find((item) => item.unique === file.unique); - if (queueItem) { - file.queueItem = queueItem; - } - return file; - }); - }); - } - - async #setFilePaths() { - await this.#serverUrlPromise; - - this.keys.forEach((key) => { - if (!UmbId.validate(key) && key.startsWith('/')) { - this._files.push({ - path: this.#serverUrl + key, - unique: UmbId.new(), - }); - this.requestUpdate(); - } - }); - } - - #setExtensions(value: Array) { - if (!value) { + #setExtensions(extensions: Array) { + if (!extensions?.length) { this._extensions = undefined; return; } - // TODO: The dropzone uui component does not support file extensions without a dot. Remove this when it does. - this._extensions = value?.map((extension) => { - return `.${extension}`; - }); + this._extensions = extensions?.map((extension) => `.${extension}`); } - #onUpload(e: UUIFileDropzoneEvent) { - const files: File[] = e.detail.files; + async #onUpload(e: UUIFileDropzoneEvent) { + //Property Editor for Upload field will always only have one file. + const item: UmbTemporaryFileModel = { + unique: UmbId.new(), + file: e.detail.files[0], + }; + const upload = this.#manager.uploadOne(item); - if (!files?.length) return; + const reader = new FileReader(); + reader.onload = () => { + this._src = reader.result as string; + }; + reader.readAsDataURL(item.file); - // TODO: Should we validate the mimetype some how? - this.#setFiles(files); - } - - #setFiles(files: File[]) { - const items = files.map( - (file): UmbTemporaryFileModel => ({ - unique: UmbId.new(), - file, - }), - ); - this.#manager.upload(items); - - this.keys = items.map((item) => item.unique!); - this.value = this.keys.join(','); - - this.dispatchEvent(new UmbChangeEvent()); - - // Read files to get their paths and add them to the file paths array. - items.forEach((item) => { - this._files.push({ - path: '', - unique: item.unique, - queueItem: item, - file: item.file, - }); - const reader = new FileReader(); - reader.onload = () => { - this._files = this._files.map((file) => { - if (file.unique === item.unique) { - file.path = reader.result as string; - } - return file; - }); - this.requestUpdate(); - }; - reader.readAsDataURL(item.file); - }); + const uploaded = await upload; + if (uploaded.status === TemporaryFileStatus.SUCCESS) { + this.temporaryFile = { unique: item.unique, file: item.file }; + this.dispatchEvent(new UmbChangeEvent()); + } } #handleBrowse() { @@ -184,77 +92,51 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen } render() { - return html` -
${this.#renderFiles()}
- ${this.#renderDropzone()} ${this.#renderButtonRemove()} - `; + return html`${this._src ? this.#renderFile(this._src, this.temporaryFile?.file) : this.#renderDropzone()}`; } - //TODO When the property editor gets saved, it seems that the property editor gets the file path from the server rather than key/id. - // This however does not work when there is multiple files. Can the server not handle multiple files uploaded into one property editor? #renderDropzone() { - if (!this.multiple && this._files.length) return nothing; - return html` + accept="${ifDefined(this._extensions?.join(', '))}"> `; } - #renderFiles() { - return repeat( - this._files, - (path) => path, - (path) => this.#renderFile(path), - ); - } - - #renderFile(file: { path: string; unique: string; queueItem?: UmbTemporaryFileModel; file?: File }) { - // TODO: Get the mime type from the server and use that to determine the file type. - const type = this.#getFileTypeFromPath(file.path); + #renderFile(src: string, tempFile?: File) { + const extension = tempFile ? mime.getExtension(tempFile.type) : this.#getFileExtensionFromPath(src); return html`
${getElementTemplate()} - ${file.queueItem?.status === 'waiting' ? html`` : nothing} + ${this.temporaryFile?.status === TemporaryFileStatus.WAITING + ? html`` + : nothing}
+ ${this.#renderButtonRemove()} `; function getElementTemplate() { - switch (type) { + switch (extension) { case 'audio': - return html``; + return html``; case 'video': - return html``; + return html``; case 'image': - return html``; + return html``; case 'svg': - return html``; - case 'file': - return html``; + return html``; + default: + return html``; } } } - #getFileTypeFromPath(path: string): 'audio' | 'video' | 'image' | 'svg' | 'file' { - // Extract the MIME type from the data URL - if (path.startsWith('data:')) { - const mimeType = path.substring(5, path.indexOf(';')); - if (mimeType === 'image/svg+xml') return 'svg'; - if (mimeType.startsWith('image/')) return 'image'; - if (mimeType.startsWith('audio/')) return 'audio'; - if (mimeType.startsWith('video/')) return 'video'; - } - - // Extract the file extension from the path + #getFileExtensionFromPath(path: string): 'audio' | 'video' | 'image' | 'svg' | 'file' { const extension = path.split('.').pop()?.toLowerCase(); if (!extension) return 'file'; if (['svg'].includes(extension)) return 'svg'; @@ -266,20 +148,14 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen } #renderButtonRemove() { - if (!this._files.length) return; - return html` ${this.localize.term('content_uploadClear')} `; } #handleRemove() { - const uniques = this._files.map((file) => file.unique); - this.#manager.remove(uniques); - this._files = []; - this.value = ''; - this.keys = []; - + this._src = ''; + this.temporaryFile = undefined; this.dispatchEvent(new UmbChangeEvent()); } @@ -296,6 +172,7 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen gap: var(--uui-size-space-4); box-sizing: border-box; } + #wrapper:has(umb-input-upload-field-file) { padding: var(--uui-size-space-4); border: 1px solid var(--uui-color-border); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts index 31e223a69b..344ca3598b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts @@ -1,45 +1,42 @@ import type { UmbInputUploadFieldElement } from '../../core/components/input-upload-field/input-upload-field.element.js'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent, type UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; +export interface MediaValueType { + temporaryFileId?: string | null; + src?: string; +} /** * @element umb-property-editor-ui-upload-field */ @customElement('umb-property-editor-ui-upload-field') export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - value = ''; - - public set config(config: UmbPropertyEditorConfigCollection | undefined) { - if (!config) return; - - this._fileExtensions = config.getValueByAlias>('fileExtensions'); - - this._multiple = config.getValueByAlias('multiple'); - } + @property({ type: Object }) + value: MediaValueType = {}; @state() private _fileExtensions?: Array; - @state() - private _multiple?: boolean; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + if (!config) return; + this._fileExtensions = config.getValueByAlias>('fileExtensions'); + } - private _onChange(event: CustomEvent) { - this.value = (event.target as unknown as UmbInputUploadFieldElement).value as string; + #onChange(event: CustomEvent) { + this.value = (event.target as UmbInputUploadFieldElement).value; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } render() { return html``; + .value=${this.value}>`; } } From af5b0f2e61f01b300ad19302dafa2d2caaf911de Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 3 May 2024 16:15:42 +0200 Subject: [PATCH 023/134] renaming --- .../media/dropzone/dropzone-manager.class.ts | 36 ++++++++++++++++--- .../media/media/dropzone/dropzone.element.ts | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index b95437bc2d..8a96ccec06 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -25,6 +25,12 @@ export interface UmbUploadableExtensionModel { mediaTypes: Array; } +/** + * Manages the dropzone and uploads files to the server. + * @method createFilesAsMedia - Upload files to the server and creates the items using corresponding media type. + * @method createFilesAsTemporary - Upload the files as temporary files and returns the data. + * @observable completed - Emits an array of completed uploads. + */ export class UmbDropzoneManager extends UmbControllerBase { #host; @@ -33,7 +39,7 @@ export class UmbDropzoneManager extends UmbControllerBase { #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); #mediaDetailRepository = new UmbMediaDetailRepository(this); - #completed = new UmbArrayState([], (upload) => upload.unique); + #completed = new UmbArrayState([], (upload) => upload.unique); public readonly completed = this.#completed.asObservable(); constructor(host: UmbControllerHost) { @@ -41,7 +47,31 @@ export class UmbDropzoneManager extends UmbControllerBase { this.#host = host; } - public async dropFiles(files: Array, parentUnique: string | null) { + /** + * Uploads the files as temporary files and returns the data. + * @param files + * @returns Promise> + */ + public async createFilesAsTemporary(files: Array): Promise> { + this.#completed.setValue([]); + const temporaryFiles: Array = []; + + for (const file of files) { + const uploaded = await this.#tempFileManager.uploadOne({ unique: UmbId.new(), file }); + this.#completed.setValue([...this.#completed.getValue(), uploaded]); + temporaryFiles.push(uploaded); + } + + return temporaryFiles; + } + + /** + * Uploads files to the server and creates the items with corresponding media type. + * Allows the user to pick a media type option if multiple types are allowed. + * @param files + * @param parentUnique + */ + public async createFilesAsMedia(files: Array, parentUnique: string | null) { if (!files.length) return; if (files.length === 1) return this.#handleOneOneFile(files[0], parentUnique); @@ -151,9 +181,7 @@ export class UmbDropzoneManager extends UmbControllerBase { } async #handleUpload(files: Array, parentUnique: string | null) { - let index: number = 0; for (const file of files) { - index++; const upload = (await this.#tempFileManager.uploadOne(file)) as UmbUploadableFileModel; if (upload.status === TemporaryFileStatus.SUCCESS) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 78196e5b3f..d88040d01a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -64,7 +64,7 @@ export class UmbDropzoneElement extends UmbLitElement { '_observeCompleted', ); //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. - await dropzoneManager.dropFiles(files, this.parentUnique); + await dropzoneManager.createFilesAsMedia(files, this.parentUnique); } render() { From 2a7e4d9f7c3dcb12734eb79cd5881bd64a12922b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:17:20 +0200 Subject: [PATCH 024/134] move hideTreeRoot to tree picker interface --- src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts | 1 - .../packages/core/tree/tree-picker/tree-picker-modal.token.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts index ac1b193cb6..992fb667a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts @@ -1,6 +1,5 @@ export interface UmbPickerModalData { multiple?: boolean; - hideTreeRoot?: boolean; // TODO: this should be moved to a tree picker modal data interface filter?: (item: ItemType) => boolean; pickableFilter?: (item: ItemType) => boolean; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts index 0f00085f53..dc4f61b5bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts @@ -15,6 +15,7 @@ export interface UmbTreePickerModalData< TreeItemType, PathPatternParamsType extends UmbPathPatternParamsType = UmbPathPatternParamsType, > extends UmbPickerModalData { + hideTreeRoot?: boolean; treeAlias?: string; // Consider if it makes sense to move this into the UmbPickerModalData interface, but for now this is a TreePicker feature. [NL] createAction?: UmbTreePickerModalCreateActionData; From 8be0853120e11f1b26422bb7b04955cca0304501 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:50:44 +0200 Subject: [PATCH 025/134] add more strict typing to picker context --- .../core/picker-input/picker-input.context.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 08519dbb51..e77f304ad4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -8,13 +8,14 @@ import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@um type PickerItemBaseType = { name: string; unique: string }; export class UmbPickerInputContext< - ItemType extends PickerItemBaseType, - TreeItemType extends PickerItemBaseType = ItemType, + PickedItemType extends PickerItemBaseType, + PickerItemType extends PickerItemBaseType = PickedItemType, + PickerModalConfigType extends UmbPickerModalData = UmbPickerModalData, + PickerModalValueType extends UmbPickerModalValue = UmbPickerModalValue, > extends UmbControllerBase { - // TODO: We are way too unsecure about the requirements for the Modal Token, as we have certain expectation for the data and value. - modalAlias: string | UmbModalToken, UmbPickerModalValue>; - repository?: UmbItemRepository; - #getUnique: (entry: ItemType) => string | undefined; + modalAlias: string | UmbModalToken, PickerModalValueType>; + repository?: UmbItemRepository; + #getUnique: (entry: PickedItemType) => string | undefined; #itemManager; @@ -48,14 +49,14 @@ export class UmbPickerInputContext< constructor( host: UmbControllerHost, repositoryAlias: string, - modalAlias: string | UmbModalToken, UmbPickerModalValue>, - getUniqueMethod?: (entry: ItemType) => string | undefined, + modalAlias: string | UmbModalToken, PickerModalValueType>, + getUniqueMethod?: (entry: PickedItemType) => string | undefined, ) { super(host); this.modalAlias = modalAlias; this.#getUnique = getUniqueMethod || ((entry) => entry.unique); - this.#itemManager = new UmbRepositoryItemsManager(this, repositoryAlias, this.#getUnique); + this.#itemManager = new UmbRepositoryItemsManager(this, repositoryAlias, this.#getUnique); this.selection = this.#itemManager.uniques; this.selectedItems = this.#itemManager.items; @@ -70,7 +71,7 @@ export class UmbPickerInputContext< this.#itemManager.setUniques(selection.filter((value) => value !== null) as Array); } - async openPicker(pickerData?: Partial>) { + async openPicker(pickerData?: Partial) { await this.#itemManager.init; const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modalContext = modalManager.open(this, this.modalAlias, { From e7f305e9cd42c6156af1464b165449ad1e92cc6b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:50:57 +0200 Subject: [PATCH 026/134] add types to data type context --- .../data-type-input/data-type-input.context.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts index 89c582df5a..ce3caee1d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts @@ -1,13 +1,17 @@ import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbDataTypeItemModel } from '../../repository/item/types.js'; +import type { UmbDataTypePickerModalData } from '../../modals/index.js'; import { UMB_DATA_TYPE_PICKER_MODAL } from '../../modals/index.js'; +import type { UmbDataTypeTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbDataTypePickerContext extends UmbPickerInputContext { +export class UmbDataTypePickerContext extends UmbPickerInputContext< + UmbDataTypeItemModel, + UmbDataTypeTreeItemModel, + UmbDataTypePickerModalData +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DATA_TYPE_PICKER_MODAL); } } From c2d84d3da828f52ef42e671089d007a65ad7652f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:51:16 +0200 Subject: [PATCH 027/134] add types to document picker context --- .../components/input-document/input-document.context.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts index 81476fe796..7f31b74cf7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts @@ -1,10 +1,17 @@ +import type { UmbDocumentPickerModalData, UmbDocumentPickerModalValue } from '../../modals/index.js'; import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/index.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbDocumentItemModel } from '../../repository/index.js'; +import type { UmbDocumentTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbDocumentPickerContext extends UmbPickerInputContext { +export class UmbDocumentPickerContext extends UmbPickerInputContext< + UmbDocumentItemModel, + UmbDocumentTreeItemModel, + UmbDocumentPickerModalData, + UmbDocumentPickerModalValue +> { constructor(host: UmbControllerHost) { super(host, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_PICKER_MODAL, (entry) => entry.unique); } From f877d4f6b0f7cbed14cb21407308a5bf3a649dce Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:53:23 +0200 Subject: [PATCH 028/134] add types to member type picker context --- .../input-member-type/input-member-type.context.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts index 30b7ab1cea..f619b2c031 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts @@ -1,9 +1,20 @@ +import type { + UmbMemberTypePickerModalData, + UmbMemberTypePickerModalValue, +} from '../../modal/member-type-picker-modal.token.js'; import { UMB_MEMBER_TYPE_PICKER_MODAL } from '../../modal/member-type-picker-modal.token.js'; +import type { UmbMemberTypeItemModel } from '../../repository/item/types.js'; +import type { UmbMemberTypeTreeItemModel } from '@umbraco-cms/backoffice/member-type'; import { UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/member-type'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMemberTypePickerContext extends UmbPickerInputContext { +export class UmbMemberTypePickerContext extends UmbPickerInputContext< + UmbMemberTypeItemModel, + UmbMemberTypeTreeItemModel, + UmbMemberTypePickerModalData, + UmbMemberTypePickerModalValue +> { constructor(host: UmbControllerHost) { super(host, UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_TYPE_PICKER_MODAL); } From b36557f9011b18be13bf2b9ab8bfbe50e4a93a5b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:55:57 +0200 Subject: [PATCH 029/134] add typing to media picker context --- .../components/input-media/input-media.context.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts index a531ea761a..ae6fa4a43f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts @@ -1,13 +1,21 @@ import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbMediaItemModel } from '../../repository/item/types.js'; +import type { UmbMediaTreeItemModel } from '../../tree/index.js'; import { UMB_MEDIA_TREE_PICKER_MODAL } from '../../tree/index.js'; +import type { + UmbMediaTreePickerModalData, + UmbMediaTreePickerModalValue, +} from '../../tree/media-tree-picker-modal.token.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMediaPickerContext extends UmbPickerInputContext { +export class UmbMediaPickerContext extends UmbPickerInputContext< + UmbMediaItemModel, + UmbMediaTreeItemModel, + UmbMediaTreePickerModalData, + UmbMediaTreePickerModalValue +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL); } } From a36318f010e0bbb75618db865b6cf61182b5feac Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 09:57:57 +0200 Subject: [PATCH 030/134] add types for media tree picker context --- .../input-media-type/input-media-type.context.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts index dfe3fea1f2..61f98a0a1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts @@ -1,13 +1,21 @@ import type { UmbMediaTypeItemModel } from '../../repository/index.js'; import { UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; +import type { + UmbMediaTypePickerModalData, + UmbMediaTypePickerModalValue, +} from '../../tree/media-type-picker-modal.token.js'; import { UMB_MEDIA_TYPE_PICKER_MODAL } from '../../tree/media-type-picker-modal.token.js'; +import type { UmbMediaTypeTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMediaTypePickerContext extends UmbPickerInputContext { +export class UmbMediaTypePickerContext extends UmbPickerInputContext< + UmbMediaTypeItemModel, + UmbMediaTypeTreeItemModel, + UmbMediaTypePickerModalData, + UmbMediaTypePickerModalValue +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_PICKER_MODAL); } } From 22ed017e8c078dd0d029577566a3804f60173fc5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 10:00:00 +0200 Subject: [PATCH 031/134] remove unused code --- .../document-types/modals/document-type-picker-modal.token.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts index 177b0bef5b..115868acf2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts @@ -7,9 +7,6 @@ import { UMB_TREE_PICKER_MODAL_ALIAS, } from '@umbraco-cms/backoffice/tree'; -/*export interface UmbDocumentTypePickerModalData - extends UmbTreePickerModalData {} -*/ export type UmbDocumentTypePickerModalData = UmbTreePickerModalData< UmbDocumentTypeTreeItemModel, typeof UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.PARAMS From 2ad332048b85fb6f5c9ab9d5ea34dde5b3e6c8d7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 10:00:07 +0200 Subject: [PATCH 032/134] add types --- .../input-document-type/input-document-type.context.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts index f89c8e3f70..059557f931 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts @@ -1,3 +1,4 @@ +import type { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalValue } from '../../modals/index.js'; import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '../../modals/index.js'; import type { UmbDocumentTypeItemModel } from '../../repository/index.js'; import type { UmbDocumentTypeTreeItemModel } from '../../tree/types.js'; @@ -7,11 +8,11 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbDocumentTypePickerContext extends UmbPickerInputContext< UmbDocumentTypeItemModel, - UmbDocumentTypeTreeItemModel + UmbDocumentTypeTreeItemModel, + UmbDocumentTypePickerModalData, + UmbDocumentTypePickerModalValue > { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_TYPE_PICKER_MODAL); } } From 18116bfd7858155926a3ab9fb1494738042b1f4d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 6 May 2024 10:23:22 +0200 Subject: [PATCH 033/134] type --- .../input-upload-field.element.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index 6dc715bb03..83b180257d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -107,8 +107,8 @@ export class UmbInputUploadFieldElement extends UmbLitElement { `; } - #renderFile(src: string, tempFile?: File) { - const extension = tempFile ? mime.getExtension(tempFile.type) : this.#getFileExtensionFromPath(src); + #renderFile(src: string, file?: File) { + const extension = this.#getFileExtensionFromPath(src); return html`
@@ -131,12 +131,22 @@ export class UmbInputUploadFieldElement extends UmbLitElement { case 'svg': return html``; default: - return html``; + return html``; } } } #getFileExtensionFromPath(path: string): 'audio' | 'video' | 'image' | 'svg' | 'file' { + // Extract the MIME type from the data URL + if (path.startsWith('data:')) { + const mimeType = path.substring(5, path.indexOf(';')); + if (mimeType === 'image/svg+xml') return 'svg'; + if (mimeType.startsWith('image/')) return 'image'; + if (mimeType.startsWith('audio/')) return 'audio'; + if (mimeType.startsWith('video/')) return 'video'; + } + + // Extract the file extension from the path const extension = path.split('.').pop()?.toLowerCase(); if (!extension) return 'file'; if (['svg'].includes(extension)) return 'svg'; From cd624764ff3fa1a2b043106a42c260c7c88c29f8 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 6 May 2024 10:50:05 +0200 Subject: [PATCH 034/134] file --- .../input-upload-field-file.element.ts | 12 ++++++++---- .../input-upload-field/input-upload-field.element.ts | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts index ecb8612d3f..1d6318058b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts @@ -2,6 +2,7 @@ import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { mime } from '@umbraco-cms/backoffice/external/mime'; @customElement('umb-input-upload-field-file') export class UmbInputUploadFieldFileElement extends UmbLitElement { @@ -36,9 +37,9 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement { protected updated(_changedProperties: PropertyValueMap | Map): void { super.updated(_changedProperties); - if (_changedProperties.has('file')) { - this.extension = this.file?.name.split('.').pop() || ''; - this.label = this.file?.name || 'loading...'; + if (_changedProperties.has('file') && this.file) { + this.extension = mime.getExtension(this.file.type) ?? ''; + this.label = this.file.name || 'loading...'; } if (_changedProperties.has('path')) { @@ -52,7 +53,10 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement { } #renderLabel() { - if (this.path) return html`${this.label}`; + if (this.path) { + // Don't make it a link if it's a temp file upload. + return this.file ? this.label : html`${this.label}`; + } return html`${this.label}`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index 83b180257d..7f14d170a2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -14,7 +14,6 @@ import { } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { mime } from '@umbraco-cms/backoffice/external/mime'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import './input-upload-field-file.element.js'; From 227a7e78e3073bf374b4f6fa7e80c51059ee9b54 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 10:59:11 +0200 Subject: [PATCH 035/134] temp ignore --- .../core/components/input-entity/input-entity.element.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts index 6800377a71..f48f3dd17f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts @@ -125,6 +125,10 @@ export class UmbInputEntityElement extends UUIFormControlMixin(UmbLitElement, '' #openPicker() { this.#pickerContext?.openPicker({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // TODO: ignoring this for now to prevent breaking existing functionality. + // if we want a very generic input it should be possible to pass in picker config hideTreeRoot: true, }); } From f15ba0086831e20369f3fd15c83dac88d763b197 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 6 May 2024 11:44:03 +0200 Subject: [PATCH 036/134] fix svg --- .../input-upload-field/input-upload-field.element.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index 7f14d170a2..d3936e118d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -110,11 +110,13 @@ export class UmbInputUploadFieldElement extends UmbLitElement { const extension = this.#getFileExtensionFromPath(src); return html` -
- ${getElementTemplate()} - ${this.temporaryFile?.status === TemporaryFileStatus.WAITING - ? html`` - : nothing} +
+
+ ${getElementTemplate()} + ${this.temporaryFile?.status === TemporaryFileStatus.WAITING + ? html`` + : nothing} +
${this.#renderButtonRemove()} `; From d455a5340e7aee2c14bb047b31e4103f5888a274 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 6 May 2024 11:53:40 +0200 Subject: [PATCH 037/134] rename to allowedFileExtensions --- .../input-upload-field/input-upload-field.element.ts | 4 ++-- .../upload-field/property-editor-ui-upload-field.element.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index d3936e118d..086630bcd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -34,10 +34,10 @@ export class UmbInputUploadFieldElement extends UmbLitElement { * @default undefined */ @property({ type: Array }) - set fileExtensions(value: Array) { + set allowedFileExtensions(value: Array) { this.#setExtensions(value); } - get fileExtensions(): Array | undefined { + get allowedFileExtensions(): Array | undefined { return this._extensions; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts index 344ca3598b..d3663ea7a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/upload-field/property-editor-ui-upload-field.element.ts @@ -35,7 +35,7 @@ export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement impleme render() { return html``; } } From 899750d085d4c77bff40bc3a47bb95ad505d10dc Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 6 May 2024 12:03:25 +0200 Subject: [PATCH 038/134] cropper align to endpoint --- .../input-image-cropper/image-cropper.element.ts | 13 ++----------- .../input-image-cropper.element.ts | 5 +++-- .../media/components/input-image-cropper/types.ts | 1 + 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts index 089620dfd0..6599c9a769 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts @@ -1,16 +1,7 @@ import type { UmbImageCropperCrop, UmbImageCropperFocalPoint } from './index.js'; import { calculateExtrapolatedValue, clamp, inverseLerp, lerp } from '@umbraco-cms/backoffice/utils'; -import type { - PropertyValueMap} from '@umbraco-cms/backoffice/external/lit'; -import { - customElement, - property, - query, - state, - LitElement, - css, - html, -} from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, property, query, state, LitElement, css, html } from '@umbraco-cms/backoffice/external/lit'; @customElement('umb-image-cropper') export class UmbImageCropperElement extends LitElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts index 673440b690..d5e6900e27 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts @@ -20,6 +20,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { @property({ attribute: false }) value: UmbImageCropperPropertyEditorValue = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, @@ -53,7 +54,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.file = file; this.fileUnique = unique; - this.value = assignToFrozenObject(this.value, { src: unique }); + this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); this.#manager?.uploadOne({ unique, file }); @@ -66,7 +67,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { } #onRemove = () => { - this.value = assignToFrozenObject(this.value, { src: '' }); + this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null }); if (!this.fileUnique) return; this.#manager?.removeOne(this.fileUnique); this.fileUnique = undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts index 95117bcc12..649368b8a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts @@ -1,4 +1,5 @@ export type UmbImageCropperPropertyEditorValue = { + temporaryFileId?: string | null; crops: Array<{ alias: string; coordinates?: { From d8f6347aaab2a3942f441fbe182441d34f40051d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 12:30:41 +0200 Subject: [PATCH 039/134] add null check for fallback key --- .../src/packages/core/utils/object/deep-merge.function.ts | 2 +- .../content-picker/property-editor-ui-content-picker.element.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts index 94b67c9b94..2031517316 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts @@ -13,7 +13,7 @@ export function umbDeepMerge< for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined) { - if (source[key]?.constructor === Object && fallback[key].constructor === Object) { + if (source[key]?.constructor === Object && fallback[key]?.constructor === Object) { result[key] = umbDeepMerge(source[key] as any, fallback[key]); } else { result[key] = source[key] as any; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index ac486f206e..1e4adec1c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -27,6 +27,7 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple @state() min = 0; + _max = Infinity; @state() max = Infinity; From 3a5207d87294c386fdf6ed3142533b8a5d6102d5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 12:31:01 +0200 Subject: [PATCH 040/134] add start from to tree picker interface --- src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts | 2 +- .../src/packages/core/tree/tree-picker/index.ts | 6 +++++- .../core/tree/tree-picker/tree-picker-modal.token.ts | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts index a6376a6b04..f900e78b84 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts @@ -12,7 +12,7 @@ export { UmbRequestReloadTreeItemChildrenEvent, } from './reload-tree-item-children/index.js'; -export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker/index.js'; +export type { UmbTreePickerModalData, UmbTreePickerModalValue, UmbTreePickerStartFrom } from './tree-picker/index.js'; export { UMB_TREE_PICKER_MODAL, UMB_TREE_PICKER_MODAL_ALIAS } from './tree-picker/index.js'; export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts index 7cbd303dac..be1bb6748f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts @@ -1,3 +1,7 @@ export { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; -export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; +export type { + UmbTreePickerModalData, + UmbTreePickerModalValue, + UmbTreePickerStartFrom, +} from './tree-picker-modal.token.js'; export { UMB_TREE_PICKER_MODAL } from './tree-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts index dc4f61b5bb..cad0b7dfce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts @@ -11,6 +11,10 @@ export interface UmbTreePickerModalCreateActionData; + startFrom?: UmbTreePickerStartFrom; } export interface UmbTreePickerModalValue extends UmbPickerModalValue {} From 485cef9d992039a34dc8c85c3f5cc80abb449682 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 12:57:37 +0200 Subject: [PATCH 041/134] export from module root --- src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts | 2 +- .../src/packages/core/tree/tree-picker/index.ts | 6 +----- .../core/tree/tree-picker/tree-picker-modal.token.ts | 7 ++----- src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts | 4 ++++ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts index f900e78b84..a6376a6b04 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/index.ts @@ -12,7 +12,7 @@ export { UmbRequestReloadTreeItemChildrenEvent, } from './reload-tree-item-children/index.js'; -export type { UmbTreePickerModalData, UmbTreePickerModalValue, UmbTreePickerStartFrom } from './tree-picker/index.js'; +export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker/index.js'; export { UMB_TREE_PICKER_MODAL, UMB_TREE_PICKER_MODAL_ALIAS } from './tree-picker/index.js'; export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts index be1bb6748f..7cbd303dac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/index.ts @@ -1,7 +1,3 @@ export { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; -export type { - UmbTreePickerModalData, - UmbTreePickerModalValue, - UmbTreePickerStartFrom, -} from './tree-picker-modal.token.js'; +export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; export { UMB_TREE_PICKER_MODAL } from './tree-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts index cad0b7dfce..9bdd2d9639 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts @@ -1,3 +1,4 @@ +import type { UmbTreeStartFrom } from '../types.js'; import { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; import type { UmbPickerModalData, UmbPickerModalValue, UmbWorkspaceModalData } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; @@ -11,10 +12,6 @@ export interface UmbTreePickerModalCreateActionData; - startFrom?: UmbTreePickerStartFrom; + startFrom?: UmbTreeStartFrom; } export interface UmbTreePickerModalValue extends UmbPickerModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts index 8ca974ed9d..d4e14abfa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts @@ -20,3 +20,7 @@ export type UmbTreeSelectionConfiguration = { selectable?: boolean; selection?: Array; }; + +export interface UmbTreeStartFrom { + unique: string; +} From e0469ec5a81b98574ab389708c9c78fe58066d67 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 13:38:59 +0200 Subject: [PATCH 042/134] set the start from config on default tree element --- .../core/tree/default/default-tree.element.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index ca9600010e..2e719dcd47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -1,4 +1,4 @@ -import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js'; +import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration, UmbTreeStartFrom } from '../types.js'; import type { UmbDefaultTreeContext } from './default-tree.context.js'; import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; @@ -22,6 +22,9 @@ export class UmbDefaultTreeElement extends UmbLitElement { @property({ type: Boolean, attribute: false }) hideTreeRoot: boolean = false; + @property({ type: Object, attribute: false }) + startFrom?: UmbTreeStartFrom; + @property({ attribute: false }) selectableFilter: (item: UmbTreeItemModelBase) => boolean = () => true; @@ -84,6 +87,10 @@ export class UmbDefaultTreeElement extends UmbLitElement { if (_changedProperties.has('filter')) { this.#treeContext!.filter = this.filter; } + + if (_changedProperties.has('startFrom')) { + this.#treeContext!.startFrom = this.startFrom; + } } getSelection() { @@ -104,7 +111,7 @@ export class UmbDefaultTreeElement extends UmbLitElement { } #renderRootItems() { - // only shot the root items directly if the tree root is hidden + // only show the root items directly if the tree root is hidden if (this.hideTreeRoot === true) { return html` ${repeat( From fd10585c501f612c7896929549c39e8148f3de18 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 13:40:08 +0200 Subject: [PATCH 043/134] pass start from --- ...operty-editor-ui-content-picker.element.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index 1e4adec1c3..ce17f47811 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -20,29 +20,27 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple value: UmbInputContentElement['items'] = []; @state() - type: UmbContentPickerSource['type'] = 'content'; + _type: UmbContentPickerSource['type'] = 'content'; @state() - startNodeId?: string | null; + _min = 0; @state() - min = 0; _max = Infinity; @state() - max = Infinity; + _allowedContentTypeUniques?: string | null; @state() - allowedContentTypeIds?: string | null; + _showOpenButton?: boolean; @state() - showOpenButton?: boolean; + _ignoreUserStartNodes?: boolean; @state() - ignoreUserStartNodes?: boolean; + _rootUnique?: string | null; #dynamicRoot?: UmbContentPickerSource['dynamicRoot']; - #dynamicRootRepository = new UmbContentPickerDynamicRootRepository(this); public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -50,27 +48,27 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple const startNode = config.getValueByAlias('startNode'); if (startNode) { - this.type = startNode.type; - this.startNodeId = startNode.id; + this._type = startNode.type; + this._rootUnique = startNode.id; this.#dynamicRoot = startNode.dynamicRoot; } - this.min = Number(config.getValueByAlias('minNumber')) || 0; - this.max = Number(config.getValueByAlias('maxNumber')) || Infinity; + this._min = Number(config.getValueByAlias('minNumber')) || 0; + this._max = Number(config.getValueByAlias('maxNumber')) || Infinity; - this.allowedContentTypeIds = config.getValueByAlias('filter'); - this.showOpenButton = config.getValueByAlias('showOpenButton'); - this.ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes'); + this._allowedContentTypeUniques = config.getValueByAlias('filter'); + this._showOpenButton = config.getValueByAlias('showOpenButton'); + this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes'); } connectedCallback() { super.connectedCallback(); - - this.#setStartNodeId(); + this.#setPickerRootUnique(); } - async #setStartNodeId() { - if (this.startNodeId) return; + async #setPickerRootUnique() { + // If we have a root unique value, we don't need to fetch it from the dynamic root + if (this._rootUnique) return; // TODO: Awaiting the workspace context to have a parent entity ID value. [LK] // e.g. const parentEntityId = this.#workspaceContext?.getParentEntityId(); @@ -79,7 +77,7 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple if (unique && this.#dynamicRoot) { const result = await this.#dynamicRootRepository.requestRoot(this.#dynamicRoot, unique); if (result && result.length > 0) { - this.startNodeId = result[0]; + this._rootUnique = result[0]; } } } @@ -90,15 +88,17 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple } render() { + const startFrom = this._rootUnique ? { unique: this._rootUnique } : undefined; + return html``; } } From 8917c3ee91cbc404c2dc7510f6555dac8577edaa Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 13:40:16 +0200 Subject: [PATCH 044/134] pass start from --- .../components/input-content/input-content.element.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts index 966114331f..02e1ecff57 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts @@ -7,6 +7,7 @@ import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; import type { UmbInputMemberElement } from '@umbraco-cms/backoffice/member'; import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models'; +import type { UmbTreeStartFrom } from '@umbraco-cms/backoffice/tree'; const elementName = 'umb-input-content'; @customElement(elementName) @@ -28,15 +29,15 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' return this._type; } - @property({ type: String }) - startNodeId?: string; - @property({ type: Number }) min = 0; @property({ type: Number }) max = 0; + @property({ type: Object, attribute: false }) + startFrom?: UmbTreeStartFrom; + private _allowedContentTypeIds: Array = []; @property() public set allowedContentTypeIds(value: string) { @@ -113,7 +114,7 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' #renderDocumentPicker() { return html` Date: Mon, 6 May 2024 13:40:28 +0200 Subject: [PATCH 045/134] pass start from --- .../components/input-document/input-document.element.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 48dd116fc8..d09fa40e55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -7,6 +7,7 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document'; +import type { UmbTreeStartFrom } from '@umbraco-cms/backoffice/tree'; @customElement('umb-input-document') export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -80,8 +81,8 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, return this.#pickerContext.getSelection(); } - @property({ type: String }) - startNodeId?: string; + @property({ type: Object, attribute: false }) + startFrom?: UmbTreeStartFrom; @property({ type: Array }) allowedContentTypeIds?: string[] | undefined; @@ -156,6 +157,7 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, this.#pickerContext.openPicker({ hideTreeRoot: true, pickableFilter: this.#pickableFilter, + startFrom: this.startFrom, }); } From 79447df91e6555f551b1810d9a5db888ea599695 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 6 May 2024 13:40:39 +0200 Subject: [PATCH 046/134] pass start from --- .../packages/core/tree/tree-picker/tree-picker-modal.element.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts index 0ba176209b..b2a5b1d438 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts @@ -103,6 +103,7 @@ export class UmbTreePickerModalElement Date: Mon, 6 May 2024 14:08:32 +0200 Subject: [PATCH 047/134] update media picker to fit dropzone and endpoints --- .../src/packages/media/media-types/index.ts | 1 + .../media/media-types/utils.ts/index.ts | 7 ++ .../media-picker-folder-path.element.ts | 12 +-- .../media/media/modals/media-picker/index.ts | 1 + .../media-picker-modal.element.ts | 89 +++++++++---------- .../media/media/modals/media-picker/types.ts | 13 +++ 6 files changed, 70 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts index 3d137a17a7..39c2b4ea8b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts @@ -5,6 +5,7 @@ export * from './workspace/index.js'; export * from './repository/index.js'; export * from './tree/types.js'; +export * from './utils.ts/index.js'; export * from './types.js'; export * from './entity.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts new file mode 100644 index 0000000000..fd3a9e966d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts @@ -0,0 +1,7 @@ +//TODO Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration() +export function getUmbracoFolderUnique(): string { + return 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d'; +} +export function isUmbracoFolder(unique?: string): boolean { + return unique === getUmbracoFolderUnique(); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts index 5a97fac2cd..d62041ce1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts @@ -1,3 +1,4 @@ +import type { UmbMediaPathModel } from '../types.js'; import type { UmbMediaDetailModel } from '../../../types.js'; import { UmbMediaDetailRepository } from '../../../repository/index.js'; import { UmbMediaTreeRepository } from '../../../tree/media-tree.repository.js'; @@ -6,13 +7,8 @@ import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbMediaTypeFileType } from '@umbraco-cms/backoffice/media-type'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; - -interface MediaPath { - name: string; - unique: string | null; -} +import { getUmbracoFolderUnique } from '@umbraco-cms/backoffice/media-type'; const root = { name: 'Media', unique: null }; @@ -40,7 +36,7 @@ export class UmbMediaPickerFolderPathElement extends UmbModalBaseElement< private _currentPath: string | null = null; @state() - private _paths: Array = [root]; + private _paths: Array = [root]; @state() private _typingNewFolder = false; @@ -89,7 +85,7 @@ export class UmbMediaPickerFolderPathElement extends UmbModalBaseElement< const preset: Partial = { unique: newUnique, mediaType: { - unique: UmbMediaTypeFileType.FOLDER, + unique: getUmbracoFolderUnique(), collection: null, }, variants: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts index 4b8bbe9334..134ffc53a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts @@ -1,3 +1,4 @@ export * from './components/index.js'; export * from './media-picker-modal.element.js'; export * from './media-picker-modal.token.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 0f7ce2d114..2871e3a290 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,4 +1,4 @@ -import type { UmbMediaUrlModel } from '../../repository/url/types.js'; +import type { UmbDropzoneElement } from '../../dropzone/dropzone.element.js'; import { UmbMediaDetailRepository, type UmbMediaItemModel, @@ -6,21 +6,14 @@ import { UmbMediaUrlRepository, } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; -import type { UmbMediaTreeItemModel } from '../../tree/types.js'; -import type { UmbDropzoneMediaElement } from '../../components/index.js'; +import type { UmbMediaCardItemModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat, query, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { isMediaTypeFolder, isMediaTypeRenderable } from '@umbraco-cms/backoffice/media-type'; - -interface MappedMediaItem - extends Partial, - Partial, - Partial { - isImageRenderable?: boolean; -} +import { isUmbracoFolder } from '@umbraco-cms/backoffice/media-type'; +import { mime } from '@umbraco-cms/backoffice/external/mime'; @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { @@ -29,10 +22,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement = []; + #mediaItemsCurrentFolder: Array = []; @state() - private _mediaFilteredList: Array = []; + private _mediaFilteredList: Array = []; @state() private _searchOnlyThisFolder = false; @@ -50,7 +43,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement): Promise> { + async #mapMediaUrls(items: Array): Promise> { if (!items.length) return []; const { data } = await this.#mediaUrlRepository.requestItems(items.map((item) => item.unique)); return items.map((item) => { - const media = data?.find((media) => media.unique === item.unique); - const isImageRenderable = isMediaTypeRenderable(item.mediaType.unique); - const isFolder = isMediaTypeFolder(item.mediaType.unique); + const url = data?.find((media) => media.unique === item.unique)?.url; + const extension = url?.split('.').pop(); + const isFolder = isUmbracoFolder(item.mediaType?.unique); + const isImageRenderable = url ? !!mime.getType(url)?.startsWith('image/') : false; - return { ...item, ...media, isImageRenderable, isFolder }; + return { name: item.name, unique: item.unique, isImageRenderable, url, isFolder, extension }; }); } - #onFolderOpen(item: MappedMediaItem) { - if (!isMediaTypeFolder(item.mediaType!.unique)) return; + #onFolderOpen(item: UmbMediaCardItemModel) { + if (!isUmbracoFolder(item.unique)) return; this._currentPath = item.unique!; this.#loadMediaFolder(); } - #onSelected(item: MappedMediaItem) { + #onSelected(item: UmbMediaCardItemModel) { const selection = this.data?.multiple ? [...this.value.selection, item.unique!] : [item.unique!]; this.modalContext?.setValue({ selection }); } - #onDeselected(item: MappedMediaItem) { + #onDeselected(item: UmbMediaCardItemModel) { const selection = this.value.selection.filter((value) => value !== item.unique); this.modalContext?.setValue({ selection }); } @@ -116,16 +105,26 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { - if (found.isTrashed) return false; - if (this._searchOnlyThisFolder) { - const isInFolder = this.#mediaItemsCurrentFolder.find((inFolder) => inFolder.unique === found.unique); - return isInFolder; - } - return true; - }); + if (!data) { + // No search results. + this._mediaFilteredList = []; + return; + } - this._mediaFilteredList = await this.#mapMediaUrls(foundItems ?? []); + if (this._searchOnlyThisFolder) { + // Don't have to map urls here, because we already have everything loaded within this folder. + this._mediaFilteredList = this.#mediaItemsCurrentFolder.filter((media) => + data.find((item) => item.unique === media.unique), + ); + return; + } + + // Map urls for search results as we are going to show for all folders (as long they aren't trashed). + this._mediaFilteredList = await this.#mapMediaUrls( + data.filter((found) => { + found.isTrashed ? false : false; + }), + ); } #onSearch(e: UUIInputEvent) { @@ -156,7 +155,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#loadMediaFolder()} .parentUnique=${this._currentPath}> + this.#loadMediaFolder()} .parentUnique=${this._currentPath}> ${ !this._mediaFilteredList.length ? html`

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

` @@ -197,7 +196,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement`; } - #renderCard(item: MappedMediaItem) { + #renderCard(item: UmbMediaCardItemModel) { return html` Date: Mon, 6 May 2024 15:44:55 +0200 Subject: [PATCH 048/134] wip implementation for dynamic root --- .../core/tree/default/default-tree.context.ts | 79 ++++++++++++++++--- .../core/tree/default/default-tree.element.ts | 13 ++- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index c55e88dd19..8432ff94a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -1,5 +1,5 @@ import { UmbRequestReloadTreeItemChildrenEvent } from '../reload-tree-item-children/index.js'; -import type { UmbTreeItemModelBase } from '../types.js'; +import type { UmbTreeItemModelBase, UmbTreeStartFrom } from '../types.js'; import type { UmbTreeRepository } from '../data/tree-repository.interface.js'; import type { UmbTreeContext } from '../tree-context.interface.js'; import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; @@ -11,9 +11,9 @@ import { import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbPaginationManager, UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbPaginationManager, UmbSelectionManager, debounce } from '@umbraco-cms/backoffice/utils'; import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; -import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -34,6 +34,12 @@ export class UmbDefaultTreeContext public readonly selection = new UmbSelectionManager(this._host); public readonly pagination = new UmbPaginationManager(); + #hideTreeRoot = new UmbBooleanState(false); + hideTreeRoot = this.#hideTreeRoot.asObservable(); + + #startFrom = new UmbObjectState(undefined); + startFrom = this.#startFrom.asObservable(); + #manifest?: ManifestTree; #repository?: UmbTreeRepository; #actionEventContext?: UmbActionEventContext; @@ -72,11 +78,9 @@ export class UmbDefaultTreeContext const unique = treeRoot.unique; if (event.detail.unique === unique) { event.stopPropagation(); - this.loadRootItems(); + this.#loadRootItems(); } }); - - this.loadTreeRoot(); } // TODO: find a generic way to do this @@ -115,18 +119,40 @@ export class UmbDefaultTreeContext return this.#repository; } - public async loadTreeRoot() { + // TODO: debouncing the load tree method because multiple props can be set at the same time + // that would trigger multiple loadTree calls. This is a temporary solution to avoid that. + public loadTree = debounce(() => this.#debouncedLoadTree(), 50); + + #debouncedLoadTree() { + const startFrom = this.getStartFrom(); + if (startFrom?.unique) { + this.#loadTreeFrom(startFrom); + return; + } + + const hideTreeRoot = this.getHideTreeRoot(); + if (hideTreeRoot) { + this.#loadRootItems(); + return; + } + + this.#loadTreeRoot(); + } + + async #loadTreeRoot() { await this.#init; + const { data } = await this.#repository!.requestTreeRoot(); if (data) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.#treeRoot.setValue(data); + this.pagination.setTotalItems(1); } } - public async loadRootItems() { + async #loadRootItems() { await this.#init; const { data } = await this.#repository!.requestRootTreeItems({ @@ -140,6 +166,39 @@ export class UmbDefaultTreeContext } } + async #loadTreeFrom(startFrom: UmbTreeStartFrom) { + await this.#init; + + const { data } = await this.#repository!.requestTreeItemsOf({ + parentUnique: startFrom.unique, + skip: this.#paging.skip, + take: this.#paging.take, + }); + + if (data) { + this.#rootItems.setValue(data.items); + this.pagination.setTotalItems(data.total); + } + } + + setHideTreeRoot(hideTreeRoot: boolean) { + this.#hideTreeRoot.setValue(hideTreeRoot); + this.loadTree(); + } + + getHideTreeRoot() { + return this.#hideTreeRoot.getValue(); + } + + setStartFrom(startFrom: UmbTreeStartFrom | undefined) { + this.#startFrom.setValue(startFrom); + this.loadTree(); + } + + getStartFrom() { + return this.#startFrom.getValue(); + } + #consumeContexts() { this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => { this.#actionEventContext = instance; @@ -157,7 +216,7 @@ export class UmbDefaultTreeContext #onPageChange = (event: UmbChangeEvent) => { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.loadRootItems(); + this.#loadRootItems(); }; #observeRepository(repositoryAlias?: string) { @@ -183,7 +242,7 @@ export class UmbDefaultTreeContext // @ts-ignore if (event.getUnique() !== treeRoot.unique) return; if (event.getEntityType() !== treeRoot.entityType) return; - this.loadRootItems(); + this.#loadRootItems(); }; destroy(): void { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index 2e719dcd47..1ad550b504 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -73,11 +73,12 @@ export class UmbDefaultTreeElement extends UmbLitElement { this.#treeContext!.selection.setSelection(this._selectionConfiguration.selection ?? []); } + if (_changedProperties.has('startFrom')) { + this.#treeContext!.setStartFrom(this.startFrom); + } + if (_changedProperties.has('hideTreeRoot')) { - if (this.hideTreeRoot === true) { - await this.#init; - this.#treeContext!.loadRootItems(); - } + this.#treeContext!.setHideTreeRoot(this.hideTreeRoot); } if (_changedProperties.has('selectableFilter')) { @@ -87,10 +88,6 @@ export class UmbDefaultTreeElement extends UmbLitElement { if (_changedProperties.has('filter')) { this.#treeContext!.filter = this.filter; } - - if (_changedProperties.has('startFrom')) { - this.#treeContext!.startFrom = this.startFrom; - } } getSelection() { From 6fa16f906374644b13ad1e517feded6e29d93c33 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 10:27:38 +0200 Subject: [PATCH 049/134] add docs --- .../core/tree/default/default-tree.context.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index 8432ff94a7..9135a2e40c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -181,24 +181,55 @@ export class UmbDefaultTreeContext } } + /** + * Sets the hideTreeRoot config + * @param {boolean} hideTreeRoot + * @memberof UmbDefaultTreeContext + */ setHideTreeRoot(hideTreeRoot: boolean) { this.#hideTreeRoot.setValue(hideTreeRoot); + // we need to reset the tree if this config changes + this.#resetTree(); this.loadTree(); } + /** + * Gets the hideTreeRoot config + * @return {boolean} + * @memberof UmbDefaultTreeContext + */ getHideTreeRoot() { return this.#hideTreeRoot.getValue(); } + /** + * Sets the startFrom config + * @param {UmbTreeStartFrom} startFrom + * @memberof UmbDefaultTreeContext + */ setStartFrom(startFrom: UmbTreeStartFrom | undefined) { this.#startFrom.setValue(startFrom); + // we need to reset the tree if this config changes + this.#resetTree(); this.loadTree(); } + /** + * Gets the startFrom config + * @return {UmbTreeStartFrom} + * @memberof UmbDefaultTreeContext + */ getStartFrom() { return this.#startFrom.getValue(); } + #resetTree() { + this.#treeRoot.setValue(undefined); + this.#rootItems.setValue([]); + this.pagination.setTotalItems(0); + this.pagination.setCurrentPageNumber(1); + } + #consumeContexts() { this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => { this.#actionEventContext = instance; From 85f5b6495dd6c130758c2ffc192c9358c2c1cf0f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 15:35:08 +0200 Subject: [PATCH 050/134] add method to clear pagination methods --- .../pagination-manager/pagination.manager.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts index 369cc39820..40fd9ca3a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts @@ -2,16 +2,22 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; export class UmbPaginationManager extends EventTarget { + #defaultValues = { + totalItems: 0, + totalPages: 1, + currentPage: 1, + }; + #pageSize = new UmbNumberState(10); public readonly pageSize = this.#pageSize.asObservable(); - #totalItems = new UmbNumberState(0); + #totalItems = new UmbNumberState(this.#defaultValues.totalItems); public readonly totalItems = this.#totalItems.asObservable(); - #totalPages = new UmbNumberState(1); + #totalPages = new UmbNumberState(this.#defaultValues.totalPages); public readonly totalPages = this.#totalPages.asObservable(); - #currentPage = new UmbNumberState(1); + #currentPage = new UmbNumberState(this.#defaultValues.currentPage); public readonly currentPage = this.#currentPage.asObservable(); #skip = new UmbNumberState(0); @@ -101,6 +107,17 @@ export class UmbPaginationManager extends EventTarget { return this.#skip.getValue(); } + /** + * Clears the pagination manager values and resets them to their default values + * @memberof UmbPaginationManager + */ + public clear() { + this.#totalItems.setValue(this.#defaultValues.totalItems); + this.#totalPages.setValue(this.#defaultValues.totalPages); + this.#currentPage.setValue(this.#defaultValues.currentPage); + this.#skip.setValue(0); + } + /** * Calculates the total number of pages * @memberof UmbPaginationManager From e4d5aeed64c4affcffec80b11b0916e309381bbf Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 15:35:31 +0200 Subject: [PATCH 051/134] implement load and reload methods --- .../core/tree/default/default-tree.context.ts | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index 9135a2e40c..d7379ff649 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -46,7 +46,7 @@ export class UmbDefaultTreeContext #paging = { skip: 0, - take: 50, + take: 3, }; #initResolver?: () => void; @@ -78,9 +78,12 @@ export class UmbDefaultTreeContext const unique = treeRoot.unique; if (event.detail.unique === unique) { event.stopPropagation(); - this.#loadRootItems(); + this.reloadTree(); } }); + + // always load the tree root because we need the root entity to reload the entire tree + this.#loadTreeRoot(); } // TODO: find a generic way to do this @@ -123,20 +126,20 @@ export class UmbDefaultTreeContext // that would trigger multiple loadTree calls. This is a temporary solution to avoid that. public loadTree = debounce(() => this.#debouncedLoadTree(), 50); - #debouncedLoadTree() { + public reloadTree = () => this.#debouncedLoadTree(true); + + #debouncedLoadTree(reload = false) { const startFrom = this.getStartFrom(); if (startFrom?.unique) { - this.#loadTreeFrom(startFrom); + this.#loadTreeFrom(startFrom, reload); return; } const hideTreeRoot = this.getHideTreeRoot(); if (hideTreeRoot) { - this.#loadRootItems(); + this.#loadRootItems(reload); return; } - - this.#loadTreeRoot(); } async #loadTreeRoot() { @@ -152,31 +155,49 @@ export class UmbDefaultTreeContext } } - async #loadRootItems() { + async #loadRootItems(reload = false) { await this.#init; + const skip = reload ? 0 : this.#paging.skip; + const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; + const { data } = await this.#repository!.requestRootTreeItems({ - skip: this.#paging.skip, - take: this.#paging.take, + skip, + take, }); if (data) { - this.#rootItems.setValue(data.items); + if (reload) { + this.#rootItems.setValue(data.items); + } else { + const currentItems = this.#rootItems.getValue(); + this.#rootItems.setValue([...currentItems, ...data.items]); + } + this.pagination.setTotalItems(data.total); } } - async #loadTreeFrom(startFrom: UmbTreeStartFrom) { + async #loadTreeFrom(startFrom: UmbTreeStartFrom, reload = false) { await this.#init; + const skip = reload ? 0 : this.#paging.skip; + const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; + const { data } = await this.#repository!.requestTreeItemsOf({ parentUnique: startFrom.unique, - skip: this.#paging.skip, - take: this.#paging.take, + skip, + take, }); if (data) { - this.#rootItems.setValue(data.items); + if (reload) { + this.#rootItems.setValue(data.items); + } else { + const currentItems = this.#rootItems.getValue(); + this.#rootItems.setValue([...currentItems, ...data.items]); + } + this.pagination.setTotalItems(data.total); } } @@ -226,8 +247,7 @@ export class UmbDefaultTreeContext #resetTree() { this.#treeRoot.setValue(undefined); this.#rootItems.setValue([]); - this.pagination.setTotalItems(0); - this.pagination.setCurrentPageNumber(1); + this.pagination.clear(); } #consumeContexts() { @@ -273,7 +293,7 @@ export class UmbDefaultTreeContext // @ts-ignore if (event.getUnique() !== treeRoot.unique) return; if (event.getEntityType() !== treeRoot.entityType) return; - this.#loadRootItems(); + this.reloadTree(); }; destroy(): void { From b346411b79b48dbcd4162ed41f1d20d988ea7e46 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 16:21:34 +0200 Subject: [PATCH 052/134] fix load, reload and pagination --- .../core/tree/default/default-tree.context.ts | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index d7379ff649..f1fe45210c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -46,7 +46,7 @@ export class UmbDefaultTreeContext #paging = { skip: 0, - take: 3, + take: 50, }; #initResolver?: () => void; @@ -122,16 +122,23 @@ export class UmbDefaultTreeContext return this.#repository; } + /** + * Loads the tree + * @memberof UmbDefaultTreeContext + */ // TODO: debouncing the load tree method because multiple props can be set at the same time // that would trigger multiple loadTree calls. This is a temporary solution to avoid that. - public loadTree = debounce(() => this.#debouncedLoadTree(), 50); + public loadTree = debounce(() => this.#debouncedLoadTree(), 100); + /** + * Reloads the tree + * @memberof UmbDefaultTreeContext + */ public reloadTree = () => this.#debouncedLoadTree(true); #debouncedLoadTree(reload = false) { - const startFrom = this.getStartFrom(); - if (startFrom?.unique) { - this.#loadTreeFrom(startFrom, reload); + if (this.getStartFrom()) { + this.#loadRootItems(reload); return; } @@ -161,34 +168,19 @@ export class UmbDefaultTreeContext const skip = reload ? 0 : this.#paging.skip; const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; - const { data } = await this.#repository!.requestRootTreeItems({ - skip, - take, - }); + // If we have a start node get children of that instead of the root + const startFrom = this.getStartFrom(); - if (data) { - if (reload) { - this.#rootItems.setValue(data.items); - } else { - const currentItems = this.#rootItems.getValue(); - this.#rootItems.setValue([...currentItems, ...data.items]); - } - - this.pagination.setTotalItems(data.total); - } - } - - async #loadTreeFrom(startFrom: UmbTreeStartFrom, reload = false) { - await this.#init; - - const skip = reload ? 0 : this.#paging.skip; - const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; - - const { data } = await this.#repository!.requestTreeItemsOf({ - parentUnique: startFrom.unique, - skip, - take, - }); + const { data } = startFrom?.unique + ? await this.#repository!.requestTreeItemsOf({ + parentUnique: startFrom.unique, + skip, + take, + }) + : await this.#repository!.requestRootTreeItems({ + skip, + take, + }); if (data) { if (reload) { From 061ca8bf10d813919e8bf064436b36389085ce32 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 16:40:53 +0200 Subject: [PATCH 053/134] rename to start node --- .../core/tree/default/default-tree.context.ts | 26 +++++++++---------- .../core/tree/default/default-tree.element.ts | 8 +++--- .../tree-picker/tree-picker-modal.element.ts | 2 +- .../tree-picker/tree-picker-modal.token.ts | 4 +-- .../src/packages/core/tree/types.ts | 2 +- .../input-document/input-document.element.ts | 6 ++--- .../input-content/input-content.element.ts | 6 ++--- ...operty-editor-ui-content-picker.element.ts | 4 +-- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index f1fe45210c..6ebffdfd98 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -1,5 +1,5 @@ import { UmbRequestReloadTreeItemChildrenEvent } from '../reload-tree-item-children/index.js'; -import type { UmbTreeItemModelBase, UmbTreeStartFrom } from '../types.js'; +import type { UmbTreeItemModelBase, UmbTreeStartNode } from '../types.js'; import type { UmbTreeRepository } from '../data/tree-repository.interface.js'; import type { UmbTreeContext } from '../tree-context.interface.js'; import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; @@ -37,8 +37,8 @@ export class UmbDefaultTreeContext #hideTreeRoot = new UmbBooleanState(false); hideTreeRoot = this.#hideTreeRoot.asObservable(); - #startFrom = new UmbObjectState(undefined); - startFrom = this.#startFrom.asObservable(); + #startNode = new UmbObjectState(undefined); + startNode = this.#startNode.asObservable(); #manifest?: ManifestTree; #repository?: UmbTreeRepository; @@ -169,11 +169,11 @@ export class UmbDefaultTreeContext const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; // If we have a start node get children of that instead of the root - const startFrom = this.getStartFrom(); + const startNode = this.getStartFrom(); - const { data } = startFrom?.unique + const { data } = startNode?.unique ? await this.#repository!.requestTreeItemsOf({ - parentUnique: startFrom.unique, + parentUnique: startNode.unique, skip, take, }) @@ -216,24 +216,24 @@ export class UmbDefaultTreeContext } /** - * Sets the startFrom config - * @param {UmbTreeStartFrom} startFrom + * Sets the startNode config + * @param {UmbTreeStartNode} startNode * @memberof UmbDefaultTreeContext */ - setStartFrom(startFrom: UmbTreeStartFrom | undefined) { - this.#startFrom.setValue(startFrom); + setStartFrom(startNode: UmbTreeStartNode | undefined) { + this.#startNode.setValue(startNode); // we need to reset the tree if this config changes this.#resetTree(); this.loadTree(); } /** - * Gets the startFrom config - * @return {UmbTreeStartFrom} + * Gets the startNode config + * @return {UmbTreeStartNode} * @memberof UmbDefaultTreeContext */ getStartFrom() { - return this.#startFrom.getValue(); + return this.#startNode.getValue(); } #resetTree() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index 1ad550b504..c089766ecd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -1,4 +1,4 @@ -import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration, UmbTreeStartFrom } from '../types.js'; +import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration, UmbTreeStartNode } from '../types.js'; import type { UmbDefaultTreeContext } from './default-tree.context.js'; import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; @@ -23,7 +23,7 @@ export class UmbDefaultTreeElement extends UmbLitElement { hideTreeRoot: boolean = false; @property({ type: Object, attribute: false }) - startFrom?: UmbTreeStartFrom; + startNode?: UmbTreeStartNode; @property({ attribute: false }) selectableFilter: (item: UmbTreeItemModelBase) => boolean = () => true; @@ -73,8 +73,8 @@ export class UmbDefaultTreeElement extends UmbLitElement { this.#treeContext!.selection.setSelection(this._selectionConfiguration.selection ?? []); } - if (_changedProperties.has('startFrom')) { - this.#treeContext!.setStartFrom(this.startFrom); + if (_changedProperties.has('startNode')) { + this.#treeContext!.setStartFrom(this.startNode); } if (_changedProperties.has('hideTreeRoot')) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts index b2a5b1d438..94fe7a3c8c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts @@ -103,7 +103,7 @@ export class UmbTreePickerModalElement; - startFrom?: UmbTreeStartFrom; + startNode?: UmbTreeStartNode; } export interface UmbTreePickerModalValue extends UmbPickerModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts index d4e14abfa3..44b27fe7f9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts @@ -21,6 +21,6 @@ export type UmbTreeSelectionConfiguration = { selection?: Array; }; -export interface UmbTreeStartFrom { +export interface UmbTreeStartNode { unique: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index d09fa40e55..059d7cfbdc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -7,7 +7,7 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document'; -import type { UmbTreeStartFrom } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; @customElement('umb-input-document') export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -82,7 +82,7 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, } @property({ type: Object, attribute: false }) - startFrom?: UmbTreeStartFrom; + startNode?: UmbTreeStartNode; @property({ type: Array }) allowedContentTypeIds?: string[] | undefined; @@ -157,7 +157,7 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, this.#pickerContext.openPicker({ hideTreeRoot: true, pickableFilter: this.#pickableFilter, - startFrom: this.startFrom, + startNode: this.startNode, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts index 02e1ecff57..6c63297ad2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts @@ -7,7 +7,7 @@ import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; import type { UmbInputMemberElement } from '@umbraco-cms/backoffice/member'; import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models'; -import type { UmbTreeStartFrom } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; const elementName = 'umb-input-content'; @customElement(elementName) @@ -36,7 +36,7 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' max = 0; @property({ type: Object, attribute: false }) - startFrom?: UmbTreeStartFrom; + startNode?: UmbTreeStartNode; private _allowedContentTypeIds: Array = []; @property() @@ -114,7 +114,7 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' #renderDocumentPicker() { return html` Date: Tue, 7 May 2024 16:50:01 +0200 Subject: [PATCH 054/134] fix pagination on tree items --- .../tree-item-base/tree-item-context-base.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 48433be491..a70321b3e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -72,7 +72,7 @@ export abstract class UmbTreeItemContextBase) { @@ -98,7 +98,7 @@ export abstract class UmbTreeItemContextBase 0); this.pagination.setTotalItems(data.total); } @@ -175,6 +184,8 @@ export abstract class UmbTreeItemContextBase this.loadChildren(true); + public toggleContextMenu() { if (!this.getTreeItem() || !this.entityType || this.unique === undefined) { throw new Error('Could not request children, tree item is not set'); @@ -302,7 +313,7 @@ export abstract class UmbTreeItemContextBase { if (event.getUnique() !== this.unique) return; if (event.getEntityType() !== this.entityType) return; - this.loadChildren(); + this.reloadChildren(); }; #onReloadStructureRequest = async (event: UmbRequestReloadStructureForEntityEvent) => { From b1e2355694e4ed0d60a6594162c47dc2421cd3f9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 7 May 2024 21:04:56 +0200 Subject: [PATCH 055/134] align methods names --- .../core/tree/default/default-tree.context.ts | 20 ++++++------ .../tree-item-base/tree-item-context-base.ts | 32 ++++++++++++------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index 6ebffdfd98..64f09d47d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -78,7 +78,7 @@ export class UmbDefaultTreeContext const unique = treeRoot.unique; if (event.detail.unique === unique) { event.stopPropagation(); - this.reloadTree(); + this.loadTree(); } }); @@ -134,7 +134,7 @@ export class UmbDefaultTreeContext * Reloads the tree * @memberof UmbDefaultTreeContext */ - public reloadTree = () => this.#debouncedLoadTree(true); + public loadMore = () => this.#debouncedLoadTree(true); #debouncedLoadTree(reload = false) { if (this.getStartFrom()) { @@ -162,11 +162,11 @@ export class UmbDefaultTreeContext } } - async #loadRootItems(reload = false) { + async #loadRootItems(loadMore = false) { await this.#init; - const skip = reload ? 0 : this.#paging.skip; - const take = reload ? this.pagination.getCurrentPageNumber() * this.#paging.take : this.#paging.take; + const skip = loadMore ? this.#paging.skip : 0; + const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take; // If we have a start node get children of that instead of the root const startNode = this.getStartFrom(); @@ -183,11 +183,11 @@ export class UmbDefaultTreeContext }); if (data) { - if (reload) { - this.#rootItems.setValue(data.items); - } else { + if (loadMore) { const currentItems = this.#rootItems.getValue(); this.#rootItems.setValue([...currentItems, ...data.items]); + } else { + this.#rootItems.setValue(data.items); } this.pagination.setTotalItems(data.total); @@ -259,7 +259,7 @@ export class UmbDefaultTreeContext #onPageChange = (event: UmbChangeEvent) => { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.#loadRootItems(); + this.loadMore(); }; #observeRepository(repositoryAlias?: string) { @@ -285,7 +285,7 @@ export class UmbDefaultTreeContext // @ts-ignore if (event.getUnique() !== treeRoot.unique) return; if (event.getEntityType() !== treeRoot.entityType) return; - this.reloadTree(); + this.loadTree(); }; destroy(): void { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index a70321b3e8..19454adb3d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -98,7 +98,7 @@ export abstract class UmbTreeItemContextBase this.#loadChildren(); + + /** + * Load more children of the tree item + * @memberof UmbTreeItemContextBase + */ + public loadMore = () => this.#loadChildren(true); + + async #loadChildren(loadMore = false) { if (this.unique === undefined) throw new Error('Could not request children, unique key is missing'); // TODO: wait for tree context to be ready const repository = this.treeContext?.getRepository(); @@ -160,8 +172,8 @@ export abstract class UmbTreeItemContextBase 0); @@ -184,8 +196,6 @@ export abstract class UmbTreeItemContextBase this.loadChildren(true); - public toggleContextMenu() { if (!this.getTreeItem() || !this.entityType || this.unique === undefined) { throw new Error('Could not request children, tree item is not set'); @@ -313,7 +323,7 @@ export abstract class UmbTreeItemContextBase { if (event.getUnique() !== this.unique) return; if (event.getEntityType() !== this.entityType) return; - this.reloadChildren(); + this.loadChildren(); }; #onReloadStructureRequest = async (event: UmbRequestReloadStructureForEntityEvent) => { @@ -340,7 +350,7 @@ export abstract class UmbTreeItemContextBase { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.loadChildren(); + this.loadMore(); }; #debouncedCheckIsActive = debounce(() => this.#checkIsActive(), 100); From 0d8e4d0b1034582d000e4edef192c80cab9633ef Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 8 May 2024 07:27:08 +0200 Subject: [PATCH 056/134] reset value --- .../tree/tree-item/tree-item-base/tree-item-context-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 19454adb3d..5c24328741 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -72,7 +72,7 @@ export abstract class UmbTreeItemContextBase) { From dcc227ee7c96d308f4711981b8502241089a3152 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 8 May 2024 10:37:05 +0200 Subject: [PATCH 057/134] add null check --- .../repository/dynamic-root.repository.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/dynamic-root/repository/dynamic-root.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/dynamic-root/repository/dynamic-root.repository.ts index e0f8e1a9af..9261eafe4b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/dynamic-root/repository/dynamic-root.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/dynamic-root/repository/dynamic-root.repository.ts @@ -40,12 +40,13 @@ export class UmbContentPickerDynamicRootRepository extends UmbControllerBase { alias: query.originAlias, id: query.originKey, }, - steps: query.querySteps!.map((step) => { - return { - alias: step.alias!, - documentTypeIds: step.anyOfDocTypeKeys!, - }; - }), + steps: + query.querySteps?.map((step) => { + return { + alias: step.alias!, + documentTypeIds: step.anyOfDocTypeKeys!, + }; + }) || [], }, }; From 730a061c8dae4e8681393d1c0f366a08c7c213fd Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 8 May 2024 12:31:32 +0200 Subject: [PATCH 058/134] everything is a folder --- .../media-picker/media-picker-modal.element.ts | 17 +++++------------ .../media-picker/media-picker-modal.token.ts | 1 - .../media/media/modals/media-picker/types.ts | 3 +-- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 2871e3a290..021e2ea06e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -39,16 +39,12 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement item.unique)); - return items.map((item) => { + return items.map((item): UmbMediaCardItemModel => { const url = data?.find((media) => media.unique === item.unique)?.url; const extension = url?.split('.').pop(); - const isFolder = isUmbracoFolder(item.mediaType?.unique); - const isImageRenderable = url ? !!mime.getType(url)?.startsWith('image/') : false; + const isImage = url ? !!mime.getType(url)?.startsWith('image/') : false; - return { name: item.name, unique: item.unique, isImageRenderable, url, isFolder, extension }; + return { name: item.name, unique: item.unique, isImage, url, extension }; }); } @@ -204,11 +199,9 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#onSelected(item)} @deselected=${() => this.#onDeselected(item)} ?selected=${this.value?.selection?.find((value) => value === item.unique)} - ?selectable=${(!item.isFolder || this._selectableFolders) && - (item.isImageRenderable || this._selectableNonImages)} - ?select-only=${!item.isFolder} + ?selectable=${item.isImage || this._selectableNonImages} file-ext=${ifDefined(item.extension)}> - ${item.isImageRenderable && item.url ? html`${ifDefined(item.name)}` : ''} + ${item.isImage && item.url ? html`${ifDefined(item.name)}` : ''} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts index 50df522088..ff01a1fc80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -2,7 +2,6 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMediaPickerModalData { startNode?: string | null; - selectableFolders?: boolean; selectableNonImages?: boolean; multiple?: boolean; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts index 1e9286488c..bcfd0358e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts @@ -3,8 +3,7 @@ export interface UmbMediaCardItemModel { unique: string; url?: string; extension?: string; - isFolder: boolean; - isImageRenderable: boolean; + isImage: boolean; } export interface UmbMediaPathModel { From b3eb22ee41ba6fbbc16657db1574f7051a3855dd Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 8 May 2024 12:53:26 +0200 Subject: [PATCH 059/134] open item not just folders --- .../media/modals/media-picker/media-picker-modal.element.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 021e2ea06e..7103bc9b65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -74,8 +74,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#onFolderOpen(item)} + @open=${() => this.#onOpen(item)} @selected=${() => this.#onSelected(item)} @deselected=${() => this.#onDeselected(item)} ?selected=${this.value?.selection?.find((value) => value === item.unique)} From e7c6d89e37b606378c14b2f084cd75d6968fa658 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 8 May 2024 15:10:26 +0200 Subject: [PATCH 060/134] create item button --- .../media-picker-folder-path.element.ts | 2 +- .../media-picker-modal.element.ts | 105 ++++++++++++++---- 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts index d62041ce1c..58b42ebc35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts @@ -139,7 +139,7 @@ export class UmbMediaPickerFolderPathElement extends UmbModalBaseElement< #path { display: flex; align-items: center; - margin-bottom: var(--uui-size-3); + margin: 0 var(--uui-size-3); } #path uui-button { font-weight: bold; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 7103bc9b65..83c8c7eb01 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,24 +1,22 @@ -import type { UmbDropzoneElement } from '../../dropzone/dropzone.element.js'; import { - UmbMediaDetailRepository, type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository, + UmbMediaDetailRepository, } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import type { UmbMediaCardItemModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { css, html, customElement, state, repeat, query, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { isUmbracoFolder } from '@umbraco-cms/backoffice/media-type'; import { mime } from '@umbraco-cms/backoffice/external/mime'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure - #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders #mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls #mediaItemRepository = new UmbMediaItemRepository(this); // used to search @@ -39,8 +37,13 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement = []; + + @state() + _popoverOpen = false; + + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); connectedCallback(): void { super.connectedCallback(); @@ -58,6 +61,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement): Promise> { @@ -134,7 +138,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement - ${this.#renderBody()} + ${this.#renderBody()} ${this.#renderPath()}
this.#loadMediaFolder()} .parentUnique=${this._currentPath}> ${ !this._mediaFilteredList.length @@ -164,14 +168,9 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement`; } - #renderPath() { - return html``; - } - #renderToolbar() { return html`
+ ${this.#renderDropdown()} - this._dropzone?.browse()}> + alert('TODO: Show media items as list/grid')} + >
`; } + // Where should this be placed, without it looking terrible? + // (this._searchOnlyThisFolder = !this._searchOnlyThisFolder)} label=${this.localize.term('general_excludeFromSubFolders')}> + + #onPopoverToggle(event: ToggleEvent) { + this._popoverOpen = event.newState === 'open'; + } + + async #getAllowedMediaTypes() { + let mediaType: string | null = null; + if (this._currentPath) { + const { data: media } = await this.#mediaItemRepository.requestItems([this._currentPath]); + mediaType = media?.[0].mediaType.unique ?? null; + } + + const { data: allowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaType); + this._allowedMediaTypes = allowedMediaTypes?.items ?? []; + } + + #renderDropdown() { + return html` + + ${this.localize.term('actions_create')} + + + + + + ${!this._allowedMediaTypes.length + ? html`
${this.localize.term('mediaPicker_notAllowed')}
` + : repeat( + this._allowedMediaTypes, + (item) => item.unique, + (item) => + html` + alert( + 'TODO: Open workspace (create) from modal. You can drop the files into this modal for now.', + )}> + + `, + )} +
+
+
+ `; + } + #renderCard(item: UmbMediaCardItemModel) { return html` `; + } + static styles = [ css` #toolbar { display: flex; gap: var(--uui-size-6); align-items: flex-start; + margin-bottom: var(--uui-size-3); } #search { flex: 1; @@ -231,6 +289,9 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement Date: Wed, 8 May 2024 15:40:56 +0200 Subject: [PATCH 061/134] fix search. move create button to different element --- .../modals/media-picker/components/index.ts | 1 + .../media-picker-create-item.element.ts | 87 +++++++++++++++++++ .../media-picker-folder-path.element.ts | 8 +- .../media-picker-modal.element.ts | 80 +---------------- 4 files changed, 93 insertions(+), 83 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts index ccae01905f..2a80f8400f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts @@ -1 +1,2 @@ export * from './media-picker-folder-path.element.js'; +export * from './media-picker-create-item.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts new file mode 100644 index 0000000000..277cb16d12 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts @@ -0,0 +1,87 @@ +import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; + +@customElement('umb-media-picker-create-item') +export class UmbMediaPickerCreateItemElement extends UmbLitElement { + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); // used to get allowed media items + + private _mediaTypeUnique: string | null = null; + @property() + public set mediaTypeUnique(value: string | null) { + this._mediaTypeUnique = value; + this.#getAllowedMediaTypes(); + } + + public get mediaTypeUnique() { + return this._mediaTypeUnique; + } + + @state() + private _popoverOpen = false; + + @state() + private _allowedMediaTypes: Array = []; + + async #getAllowedMediaTypes() { + const { data: allowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this._mediaTypeUnique); + this._allowedMediaTypes = allowedMediaTypes?.items ?? []; + } + + #onPopoverToggle(event: ToggleEvent) { + this._popoverOpen = event.newState === 'open'; + } + + render() { + return html` + + ${this.localize.term('actions_create')} + + + + + + ${!this._allowedMediaTypes.length + ? html`
${this.localize.term('mediaPicker_notAllowed')}
` + : repeat( + this._allowedMediaTypes, + (item) => item.unique, + (item) => + html` + alert( + 'TODO: Open workspace (create) from modal. You can drop the files into this modal for now.', + )}> + + `, + )} +
+
+
+ `; + } + + static styles = [ + css` + #not-allowed { + padding: var(--uui-size-space-3); + } + `, + ]; +} + +export default UmbMediaPickerCreateItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-create-item': UmbMediaPickerCreateItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts index 58b42ebc35..0dbd68bace 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts @@ -2,21 +2,17 @@ import type { UmbMediaPathModel } from '../types.js'; import type { UmbMediaDetailModel } from '../../../types.js'; import { UmbMediaDetailRepository } from '../../../repository/index.js'; import { UmbMediaTreeRepository } from '../../../tree/media-tree.repository.js'; -import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from '../media-picker-modal.token.js'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { getUmbracoFolderUnique } from '@umbraco-cms/backoffice/media-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; const root = { name: 'Media', unique: null }; @customElement('umb-media-picker-folder-path') -export class UmbMediaPickerFolderPathElement extends UmbModalBaseElement< - UmbMediaPickerModalData, - UmbMediaPickerModalValue -> { +export class UmbMediaPickerFolderPathElement extends UmbLitElement { #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 83c8c7eb01..21cd55d97b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,9 +1,4 @@ -import { - type UmbMediaItemModel, - UmbMediaItemRepository, - UmbMediaUrlRepository, - UmbMediaDetailRepository, -} from '../../repository/index.js'; +import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import type { UmbMediaCardItemModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; @@ -12,7 +7,6 @@ import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { mime } from '@umbraco-cms/backoffice/external/mime'; -import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbModalBaseElement { @@ -37,14 +31,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement = []; - - @state() - _popoverOpen = false; - - #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); - connectedCallback(): void { super.connectedCallback(); this._selectableNonImages = this.data?.selectableNonImages ?? true; @@ -61,7 +47,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement): Promise> { @@ -118,11 +103,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { - found.isTrashed ? false : false; - }), - ); + this._mediaFilteredList = await this.#mapMediaUrls(data.filter((found) => found.isTrashed === false)); } #onSearch(e: UUIInputEvent) { @@ -170,7 +151,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement - ${this.#renderDropdown()} +