From db85310624f2dd33cce2c777331ae207bc03181d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:18:14 +0100 Subject: [PATCH 1/8] Feature: TempFile upload updates --- .../input-upload-field.element.ts | 10 +-- .../temporary-file-manager.class.ts | 66 +++++++++++-------- .../collection/media-collection.element.ts | 18 ++++- .../input-image-cropper.element.ts | 2 +- 4 files changed, 59 insertions(+), 37 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 391fd80510..ad87b45ca5 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,4 +1,4 @@ -import type { TemporaryFileQueueItem } from '../../temporary-file/temporary-file-manager.class.js'; +import type { UmbTemporaryFileModel } from '../../temporary-file/temporary-file-manager.class.js'; import { UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js'; import { UMB_PROPERTY_DATASET_CONTEXT } from '../../property/property-dataset/property-dataset-context.token.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; @@ -60,7 +60,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) _files: Array<{ path: string; unique: string; - queueItem?: TemporaryFileQueueItem; + queueItem?: UmbTemporaryFileModel; file?: File; }> = []; @@ -93,8 +93,8 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) this.#serverUrl = instance.getServerUrl(); }).asPromise(); - this.observe(this.#manager.isReady, (value) => (this.error = !value)); 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) { @@ -144,7 +144,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) #setFiles(files: File[]) { const items = files.map( - (file): TemporaryFileQueueItem => ({ + (file): UmbTemporaryFileModel => ({ unique: UmbId.new(), file, status: 'waiting', @@ -216,7 +216,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) ); } - #renderFile(file: { path: string; unique: string; queueItem?: TemporaryFileQueueItem; file?: File }) { + #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); 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 d14ce52a35..a34d08fe60 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 @@ -1,38 +1,54 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; -import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; 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 = 'complete' | 'waiting' | 'error'; +export type TemporaryFileStatus = 'success' | 'waiting' | 'error'; -export interface TemporaryFileQueueItem { - unique: string; +export interface UmbTemporaryFileModel { + file: File; + unique: string; + status: TemporaryFileStatus; +} + +export interface UmbTemporaryFileQueueModel extends Partial { file: File; - status?: TemporaryFileStatus; } export class UmbTemporaryFileManager extends UmbControllerBase { #temporaryFileRepository; - #queue = new UmbArrayState([], (item) => item.unique); + #queue = new UmbArrayState([], (item) => item.unique); public readonly queue = this.#queue.asObservable(); - #isReady = new UmbBooleanState(true); - public readonly isReady = this.#isReady.asObservable(); + #filesCompleted: Array = []; constructor(host: UmbControllerHost) { super(host); this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); } - uploadOne(unique: string, file: File, status: TemporaryFileStatus = 'waiting') { - this.#queue.appendOne({ unique, file, status }); - this.handleQueue(); + uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise | undefined> { + const item: UmbTemporaryFileModel = { + file: queueItem.file, + unique: queueItem.unique ?? UmbId.new(), + status: queueItem.status ?? 'waiting', + }; + this.#queue.appendOne(item); + return this.handleQueue(); } - upload(queueItems: Array) { - this.#queue.append(queueItems); - this.handleQueue(); + upload(queueItems: Array): Promise | undefined> { + const items = queueItems.map( + (item): UmbTemporaryFileModel => ({ + file: item.file, + unique: item.unique ?? UmbId.new(), + status: item.status ?? 'waiting', + }), + ); + this.#queue.append(items); + return this.handleQueue(); } removeOne(unique: string) { @@ -44,14 +60,13 @@ export class UmbTemporaryFileManager extends UmbControllerBase { } private async handleQueue() { + this.#filesCompleted = []; const queue = this.#queue.getValue(); - if (!queue.length && this.getIsReady()) return; + if (!queue.length) return; - this.#isReady.setValue(false); - - queue.forEach(async (item) => { - if (item.status !== 'waiting') return; + for (const item of queue) { + 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 @@ -59,16 +74,11 @@ export class UmbTemporaryFileManager extends UmbControllerBase { if (error) { this.#queue.updateOne(item.unique, { ...item, status: 'error' }); } else { - this.#queue.updateOne(item.unique, { ...item, status: 'complete' }); + this.#queue.updateOne(item.unique, { ...item, status: 'success' }); } - }); - - if (!queue.find((item) => item.status === 'waiting') && !this.getIsReady()) { - this.#isReady.setValue(true); + this.#filesCompleted = [...this.#filesCompleted, item]; + this.removeOne(item.unique); } - } - - getIsReady() { - return this.#queue.getValue(); + return this.#filesCompleted; } } 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 79ca338c7a..2fe1c6e177 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 @@ -2,9 +2,14 @@ import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import './media-collection-toolbar.element.js'; +import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbTemporaryFileManager, type UmbTemporaryFileQueueModel } from '@umbraco-cms/backoffice/temporary-file'; +import { UmbId } from '@umbraco-cms/backoffice/id'; @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { + #fileManager = new UmbTemporaryFileManager(this); + constructor() { super(); document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); @@ -33,8 +38,15 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { this.toggleAttribute('dragging', false); } - #onFileChange(event: Event) { - console.log('#onFileChange', event); + async #onFileUpload(event: UUIFileDropzoneEvent) { + const files: Array = event.detail.files.map((file) => ({ file, unique: UmbId.new() })); + if (!files.length) return; + + const items = await this.#fileManager.upload(files); + if (!items) return; + + console.log('uploadComplete', items); + /*TODO Add the files to collection */ } protected renderToolbar() { @@ -44,7 +56,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { `; 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 902f1f082a..673440b690 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 @@ -55,7 +55,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.value = assignToFrozenObject(this.value, { src: unique }); - this.#manager?.uploadOne(unique, file, 'waiting'); + this.#manager?.uploadOne({ unique, file }); this.dispatchEvent(new UmbChangeEvent()); } From cadf4097e56cd5321096977f3798026176d0edf8 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:26:46 +0100 Subject: [PATCH 2/8] manager --- .../temporary-file-manager.class.ts | 15 +++++++-------- .../media/collection/media-collection.element.ts | 4 +--- 2 files changed, 8 insertions(+), 11 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 a34d08fe60..80eed3a3f9 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 @@ -22,14 +22,12 @@ export class UmbTemporaryFileManager extends UmbControllerBase { #queue = new UmbArrayState([], (item) => item.unique); public readonly queue = this.#queue.asObservable(); - #filesCompleted: Array = []; - constructor(host: UmbControllerHost) { super(host); this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); } - uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise | undefined> { + async uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise> { const item: UmbTemporaryFileModel = { file: queueItem.file, unique: queueItem.unique ?? UmbId.new(), @@ -39,7 +37,7 @@ export class UmbTemporaryFileManager extends UmbControllerBase { return this.handleQueue(); } - upload(queueItems: Array): Promise | undefined> { + async upload(queueItems: Array): Promise> { const items = queueItems.map( (item): UmbTemporaryFileModel => ({ file: item.file, @@ -60,10 +58,10 @@ export class UmbTemporaryFileManager extends UmbControllerBase { } private async handleQueue() { - this.#filesCompleted = []; + const filesCompleted: Array = []; const queue = this.#queue.getValue(); - if (!queue.length) return; + if (!queue.length) return filesCompleted; for (const item of queue) { if (!item.unique) throw new Error(`Unique is missing for item ${item}`); @@ -76,9 +74,10 @@ export class UmbTemporaryFileManager extends UmbControllerBase { } else { this.#queue.updateOne(item.unique, { ...item, status: 'success' }); } - this.#filesCompleted = [...this.#filesCompleted, item]; + filesCompleted.push(item); this.removeOne(item.unique); } - return this.#filesCompleted; + + return filesCompleted; } } 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 2fe1c6e177..ac33ba4e8e 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 @@ -1,6 +1,5 @@ import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; - import './media-collection-toolbar.element.js'; import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbTemporaryFileManager, type UmbTemporaryFileQueueModel } from '@umbraco-cms/backoffice/temporary-file'; @@ -43,10 +42,9 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { if (!files.length) return; const items = await this.#fileManager.upload(files); - if (!items) return; + if (!items.length) return; console.log('uploadComplete', items); - /*TODO Add the files to collection */ } protected renderToolbar() { From 033f303ce6f2bb571d2a85a9b8dd540eb6b48835 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:47:46 +0100 Subject: [PATCH 3/8] completed queue --- .../core/temporary-file/temporary-file-manager.class.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 80eed3a3f9..5127c55990 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 @@ -22,6 +22,9 @@ export class UmbTemporaryFileManager extends UmbControllerBase { #queue = new UmbArrayState([], (item) => item.unique); public readonly queue = this.#queue.asObservable(); + #completed = new UmbArrayState([], (item) => item.unique); + public readonly completed = this.#completed.asObservable(); + constructor(host: UmbControllerHost) { super(host); this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); @@ -75,7 +78,9 @@ export class UmbTemporaryFileManager extends UmbControllerBase { this.#queue.updateOne(item.unique, { ...item, status: 'success' }); } filesCompleted.push(item); - this.removeOne(item.unique); + + this.#completed.appendOne(item); + this.#queue.removeOne(item.unique); } return filesCompleted; From fbcb69d18fe32c7c546f0cb5f08ff973da7bcad6 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:03:16 +0100 Subject: [PATCH 4/8] dropzone upload --- .../src/packages/media/media-types/index.ts | 2 + .../packages/media/media-types/utils/index.ts | 28 +++++++ .../collection/media-collection.element.ts | 80 ++++++++++++++++--- 3 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.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 4a54461e21..60142d5186 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 @@ -6,3 +6,5 @@ export * from './workspace/index.js'; export * from './repository/index.js'; export * from './tree/types.js'; export * from './types.js'; + +export * from './utils/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 new file mode 100644 index 0000000000..b67055a38e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts @@ -0,0 +1,28 @@ +export enum UmbMediaTypeFileType { + SVG = 'Vector Graphics (SVG)', + IMAGE = 'Image', + AUDIO = 'Audio', + VIDEO = 'Video', + ARTICLE = 'Article', + FILE = 'File', +} + +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; +} 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 ac33ba4e8e..feab96f6c5 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 @@ -1,16 +1,34 @@ +import type { UmbMediaDetailModel } from '../types.js'; +import { type UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT } from '../tree/media-tree.store.js'; +import type { UmbMediaCollectionContext } from './media-collection.context.js'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; -import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; +import { UMB_DEFAULT_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import './media-collection-toolbar.element.js'; import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbTemporaryFileManager, type UmbTemporaryFileQueueModel } from '@umbraco-cms/backoffice/temporary-file'; +import { UMB_MEDIA_ENTITY_TYPE, UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; +import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { getMediaTypeByFileMimeType, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; + @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { #fileManager = new UmbTemporaryFileManager(this); + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); + #mediaDetailRepository = new UmbMediaDetailRepository(this); + + #mediaCollection?: UmbMediaCollectionContext; + #mediaTreeStore?: UmbMediaTreeStore; constructor() { super(); + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#mediaCollection = instance as UmbMediaCollectionContext; + }); + this.consumeContext(UMB_MEDIA_TREE_STORE_CONTEXT, (instance) => { + this.#mediaTreeStore = instance; + console.log('instance is here', instance); + }); document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); document.addEventListener('drop', this.#handleDrop.bind(this)); @@ -38,26 +56,66 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { } async #onFileUpload(event: UUIFileDropzoneEvent) { - const files: Array = event.detail.files.map((file) => ({ file, unique: UmbId.new() })); + /** TODO: Move dropzone and logic into its own component, so that we can reuse it in more places... */ + const files: Array = event.detail.files; if (!files.length) return; - const items = await this.#fileManager.upload(files); - if (!items.length) return; + const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(null); + if (!data) return; - console.log('uploadComplete', items); + for (const file of files) { + const mediaTypeDetailUnique = UmbId.new(); + + const mediaTypeName = getMediaTypeByFileMimeType(file.type); + const mediaType = data.items.find((type) => type.name === mediaTypeName)!; + + const mediaTempFileUnique = UmbId.new(); + + const uploaded = await this.#fileManager.uploadOne({ file, unique: mediaTempFileUnique }); + if (uploaded.find((item) => item.status === 'error')) return; + + const model: UmbMediaDetailModel = { + unique: mediaTypeDetailUnique, + mediaType: { + unique: mediaType.unique, + collection: null, + }, + entityType: UMB_MEDIA_ENTITY_TYPE, + isTrashed: false, + urls: [], + values: [ + { + alias: 'umbracoFile', + value: { src: mediaTempFileUnique }, + culture: null, + segment: null, + }, + ], + variants: [ + { + culture: null, + segment: null, + name: file.name, + createDate: '', + updateDate: '', + }, + ], + }; + + await this.#mediaDetailRepository.create(model, null); + + this.#mediaCollection?.requestCollection(); + } } protected renderToolbar() { - return html` - - + return html` - `; + accept="">`; } static styles = [ From ed690ec8884efc52f19ad9768efb858eacd45b6c Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:39:58 +0100 Subject: [PATCH 5/8] own component --- .../collection/media-collection.element.ts | 134 +------------- .../dropzone-media/dropzone-media.element.ts | 172 ++++++++++++++++++ .../media/components/dropzone-media/index.ts | 1 + .../packages/media/media/components/index.ts | 1 + 4 files changed, 175 insertions(+), 133 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts create 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/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index feab96f6c5..a06fb569bd 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 @@ -1,155 +1,23 @@ -import type { UmbMediaDetailModel } from '../types.js'; -import { type UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT } from '../tree/media-tree.store.js'; import type { UmbMediaCollectionContext } from './media-collection.context.js'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UMB_DEFAULT_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import './media-collection-toolbar.element.js'; -import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UMB_MEDIA_ENTITY_TYPE, UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; -import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; -import { UmbId } from '@umbraco-cms/backoffice/id'; - -import { getMediaTypeByFileMimeType, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { - #fileManager = new UmbTemporaryFileManager(this); - #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); - #mediaDetailRepository = new UmbMediaDetailRepository(this); - #mediaCollection?: UmbMediaCollectionContext; - #mediaTreeStore?: UmbMediaTreeStore; constructor() { super(); this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { this.#mediaCollection = instance as UmbMediaCollectionContext; }); - this.consumeContext(UMB_MEDIA_TREE_STORE_CONTEXT, (instance) => { - this.#mediaTreeStore = instance; - console.log('instance is here', instance); - }); - 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(); - console.log('#handleDrop', event); - this.toggleAttribute('dragging', false); - } - - async #onFileUpload(event: UUIFileDropzoneEvent) { - /** TODO: Move dropzone and logic into its own component, so that we can reuse it in more places... */ - const files: Array = event.detail.files; - if (!files.length) return; - - const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(null); - if (!data) return; - - for (const file of files) { - const mediaTypeDetailUnique = UmbId.new(); - - const mediaTypeName = getMediaTypeByFileMimeType(file.type); - const mediaType = data.items.find((type) => type.name === mediaTypeName)!; - - const mediaTempFileUnique = UmbId.new(); - - const uploaded = await this.#fileManager.uploadOne({ file, unique: mediaTempFileUnique }); - if (uploaded.find((item) => item.status === 'error')) return; - - const model: UmbMediaDetailModel = { - unique: mediaTypeDetailUnique, - mediaType: { - unique: mediaType.unique, - collection: null, - }, - entityType: UMB_MEDIA_ENTITY_TYPE, - isTrashed: false, - urls: [], - values: [ - { - alias: 'umbracoFile', - value: { src: mediaTempFileUnique }, - culture: null, - segment: null, - }, - ], - variants: [ - { - culture: null, - segment: null, - name: file.name, - createDate: '', - updateDate: '', - }, - ], - }; - - await this.#mediaDetailRepository.create(model, null); - - this.#mediaCollection?.requestCollection(); - } } protected renderToolbar() { return html` - `; + this.#mediaCollection?.requestCollection()}>`; } - - static styles = [ - css` - :host([dragging]) #dropzone { - opacity: 1; - pointer-events: all; - } - [dropzone] { - opacity: 0; - } - #dropzone { - opacity: 0; - pointer-events: none; - display: block; - 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 UmbMediaCollectionElement; 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 new file mode 100644 index 0000000000..3c252bed80 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts @@ -0,0 +1,172 @@ +import { UmbMediaDetailRepository } from '../../repository/index.js'; +import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; +import type { UmbMediaDetailModel } from '../../types.js'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { + type UmbAllowedMediaTypeModel, + UmbMediaTypeStructureRepository, + getMediaTypeByFileMimeType, +} from '@umbraco-cms/backoffice/media-type'; +import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { UmbChangeEvent } 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); + + @property() + collectionUnique: string | null = null; + + @property() + parentUnique: string | null = null; + + constructor() { + super(); + 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 { + const mediaTypeName = getMediaTypeByFileMimeType(mimetype); + return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; + } + + async #uploadHandler(file: File) { + const unique = UmbId.new(); + const uploaded = await this.#fileManager.uploadOne({ file, unique }); + if (uploaded[0].status === 'error') { + throw new Error('Error uploading file'); + } + return uploaded[0]; + } + + async #onFileUpload(event: UUIFileDropzoneEvent) { + const files: Array = event.detail.files; + if (!files.length) return; + + for (const file of files) { + const mediaTypeDetailUnique = UmbId.new(); + + const mediaType = this.#getMediaTypeFromMime(file.type); + const uploaded = await this.#uploadHandler(file); + + const model: UmbMediaDetailModel = { + unique: mediaTypeDetailUnique, + mediaType: { + unique: mediaType.unique, + collection: this.collectionUnique ? { unique: this.collectionUnique } : null, + }, + entityType: UMB_MEDIA_ENTITY_TYPE, + isTrashed: false, + urls: [], + values: [ + { + alias: 'umbracoFile', + value: { src: uploaded.unique }, + culture: null, + segment: null, + }, + ], + variants: [ + { + culture: null, + segment: null, + name: file.name, + createDate: '', + updateDate: '', + }, + ], + }; + await this.#mediaDetailRepository.create(model, 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 new file mode 100644 index 0000000000..a6c28ed09c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts @@ -0,0 +1 @@ +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 3c48dc1e29..d884d15b41 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,4 +1,5 @@ import './input-media/index.js'; +export * from './dropzone-media/index.js'; export * from './input-media/index.js'; export * from './input-image-cropper/index.js'; From cd82a09cf1169806a78ce5730125e6c8b96b953d Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:00:41 +0100 Subject: [PATCH 6/8] simplify --- .../dropzone-media/dropzone-media.element.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 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 3c252bed80..c0b2990cbc 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 @@ -79,20 +79,25 @@ export class UmbDropzoneMediaElement extends UmbLitElement { if (!files.length) return; for (const file of files) { - const mediaTypeDetailUnique = UmbId.new(); - const mediaType = this.#getMediaTypeFromMime(file.type); - const uploaded = await this.#uploadHandler(file); - const model: UmbMediaDetailModel = { - unique: mediaTypeDetailUnique, + const uploaded = await this.#uploadHandler(file); + /** TODO: Show uploading badge while waiting... */ + + const preset: Partial = { mediaType: { unique: mediaType.unique, collection: this.collectionUnique ? { unique: this.collectionUnique } : null, }, - entityType: UMB_MEDIA_ENTITY_TYPE, - isTrashed: false, - urls: [], + variants: [ + { + culture: null, + segment: null, + name: file.name, + createDate: null, + updateDate: null, + }, + ], values: [ { alias: 'umbracoFile', @@ -101,17 +106,12 @@ export class UmbDropzoneMediaElement extends UmbLitElement { segment: null, }, ], - variants: [ - { - culture: null, - segment: null, - name: file.name, - createDate: '', - updateDate: '', - }, - ], }; - await this.#mediaDetailRepository.create(model, null); + + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + if (!data) return; + await this.#mediaDetailRepository.create(data, null); + this.dispatchEvent(new UmbChangeEvent()); } } From 6e7574328b5774ec0fcbff69df9a33cbcf2133a3 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:43:02 +0100 Subject: [PATCH 7/8] observe temp uploads... --- .../temporary-file-manager.class.ts | 16 +++--- .../dropzone-media/dropzone-media.element.ts | 49 ++++++++++--------- 2 files changed, 33 insertions(+), 32 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 5127c55990..2f349c4bbf 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 @@ -22,15 +22,13 @@ export class UmbTemporaryFileManager extends UmbControllerBase { #queue = new UmbArrayState([], (item) => item.unique); public readonly queue = this.#queue.asObservable(); - #completed = new UmbArrayState([], (item) => item.unique); - public readonly completed = this.#completed.asObservable(); - constructor(host: UmbControllerHost) { super(host); this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); } async uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise> { + this.#queue.setValue([]); const item: UmbTemporaryFileModel = { file: queueItem.file, unique: queueItem.unique ?? UmbId.new(), @@ -41,6 +39,7 @@ export class UmbTemporaryFileManager extends UmbControllerBase { } async upload(queueItems: Array): Promise> { + this.#queue.setValue([]); const items = queueItems.map( (item): UmbTemporaryFileModel => ({ file: item.file, @@ -72,15 +71,16 @@ export class UmbTemporaryFileManager extends UmbControllerBase { 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 + let status: TemporaryFileStatus; if (error) { - this.#queue.updateOne(item.unique, { ...item, status: 'error' }); + status = 'error'; + this.#queue.updateOne(item.unique, { ...item, status }); } else { - this.#queue.updateOne(item.unique, { ...item, status: 'success' }); + status = 'success'; + this.#queue.updateOne(item.unique, { ...item, status }); } - filesCompleted.push(item); - this.#completed.appendOne(item); - this.#queue.removeOne(item.unique); + filesCompleted.push({ ...item, status }); } return filesCompleted; 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 c0b2990cbc..f05ee14351 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,16 +1,18 @@ import { UmbMediaDetailRepository } from '../../repository/index.js'; -import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; import type { UmbMediaDetailModel } from '../../types.js'; -import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository, getMediaTypeByFileMimeType, } from '@umbraco-cms/backoffice/media-type'; -import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { + UmbTemporaryFileManager, + type UmbTemporaryFileQueueModel, + type UmbTemporaryFileModel, +} from '@umbraco-cms/backoffice/temporary-file'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-dropzone-media') @@ -20,14 +22,18 @@ export class UmbDropzoneMediaElement extends UmbLitElement { #allowedMediaTypes: Array = []; #mediaDetailRepository = new UmbMediaDetailRepository(this); - @property() - collectionUnique: string | null = null; - - @property() - parentUnique: string | null = null; + @state() + private queue: Array = []; constructor() { super(); + + this.observe(this.#fileManager.queue, (queue) => { + this.queue = queue; + /** TODO: Show uploading badge while waiting... */ + console.log(queue); + }); + this.#getAllowedMediaTypes(); document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); @@ -65,35 +71,30 @@ export class UmbDropzoneMediaElement extends UmbLitElement { return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; } - async #uploadHandler(file: File) { - const unique = UmbId.new(); - const uploaded = await this.#fileManager.uploadOne({ file, unique }); - if (uploaded[0].status === 'error') { - throw new Error('Error uploading file'); - } - return uploaded[0]; + 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 file of files) { - const mediaType = this.#getMediaTypeFromMime(file.type); - - const uploaded = await this.#uploadHandler(file); - /** TODO: Show uploading badge while waiting... */ + for (const upload of uploads) { + const mediaType = this.#getMediaTypeFromMime(upload.file.type); const preset: Partial = { mediaType: { unique: mediaType.unique, - collection: this.collectionUnique ? { unique: this.collectionUnique } : null, + collection: null, }, variants: [ { culture: null, segment: null, - name: file.name, + name: upload.file.name, createDate: null, updateDate: null, }, @@ -101,7 +102,7 @@ export class UmbDropzoneMediaElement extends UmbLitElement { values: [ { alias: 'umbracoFile', - value: { src: uploaded.unique }, + value: { src: upload.unique }, culture: null, segment: null, }, From 6d5bf736b4881644210487da98ca21b26b8582e9 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Thu, 28 Mar 2024 11:56:44 +0000 Subject: [PATCH 8/8] Added `UmbProgressEvent` and wired up a ``. --- .../src/packages/core/event/index.ts | 1 + .../src/packages/core/event/progress.event.ts | 9 ++++++++ .../collection/media-collection.element.ts | 23 ++++++++++++++++--- .../dropzone-media/dropzone-media.element.ts | 8 ++++--- 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/event/progress.event.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts index af975b4ddc..7863291fc0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts @@ -3,6 +3,7 @@ export * from './change.event.js'; export * from './delete.event.js'; export * from './deselected.event.js'; export * from './input.event.js'; +export * from './progress.event.js'; export * from './selected.event.js'; export * from './selection-change.event.js'; export * from './request-reload-structure-for-entity.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/progress.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/progress.event.ts new file mode 100644 index 0000000000..8d83b5d99a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/progress.event.ts @@ -0,0 +1,9 @@ +export class UmbProgressEvent extends Event { + public static readonly TYPE = 'progress'; + public progress: number; + + public constructor(progress: number) { + super(UmbProgressEvent.TYPE, { bubbles: true, composed: false, cancelable: false }); + this.progress = progress; + } +} 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 a06fb569bd..dd74cf2241 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 @@ -1,12 +1,17 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js'; -import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +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-collection-toolbar.element.js'; @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { #mediaCollection?: UmbMediaCollectionContext; + @state() + private _progress = -1; + constructor() { super(); this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { @@ -14,9 +19,21 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { }); } + #onChange() { + this._progress = -1; + this.#mediaCollection?.requestCollection(); + } + + #onProgress(event: UmbProgressEvent) { + this._progress = event.progress; + } + protected renderToolbar() { - return html` - this.#mediaCollection?.requestCollection()}>`; + 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 f05ee14351..6a03dc5e19 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 @@ -13,7 +13,7 @@ import { type UmbTemporaryFileQueueModel, type UmbTemporaryFileModel, } from '@umbraco-cms/backoffice/temporary-file'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-dropzone-media') export class UmbDropzoneMediaElement extends UmbLitElement { @@ -30,8 +30,9 @@ export class UmbDropzoneMediaElement extends UmbLitElement { this.observe(this.#fileManager.queue, (queue) => { this.queue = queue; - /** TODO: Show uploading badge while waiting... */ - console.log(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(); @@ -111,6 +112,7 @@ export class UmbDropzoneMediaElement extends UmbLitElement { const { data } = await this.#mediaDetailRepository.createScaffold(preset); if (!data) return; + await this.#mediaDetailRepository.create(data, null); this.dispatchEvent(new UmbChangeEvent());