From 461b4849474fdc04e6d7f92581b6ccb024322b66 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 13:42:01 +0200 Subject: [PATCH 01/11] Feature: Property Editor Media Picker Thumbnails --- .../input-upload-field.element.ts | 6 +-- .../temporary-file-manager.class.ts | 13 +++--- .../input-image-cropper.element.ts | 2 +- .../input-media/input-media.context.ts | 28 +++++++++++++ .../input-media/input-media.element.ts | 42 ++++++++++++++----- .../media/dropzone/dropzone-manager.class.ts | 20 ++++++--- .../media/media/dropzone/dropzone.element.ts | 30 ++++++++++--- .../media-picker-modal.element.ts | 9 +++- .../media/media/modals/media-picker/types.ts | 3 +- 9 files changed, 120 insertions(+), 33 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 086630bcd5..6dce37947f 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 @@ -25,7 +25,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement { this._src = value.src; } get value(): MediaValueType { - return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique }; + return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.temporaryUnique }; } /** @@ -67,7 +67,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement { async #onUpload(e: UUIFileDropzoneEvent) { //Property Editor for Upload field will always only have one file. const item: UmbTemporaryFileModel = { - unique: UmbId.new(), + temporaryUnique: UmbId.new(), file: e.detail.files[0], }; const upload = this.#manager.uploadOne(item); @@ -80,7 +80,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement { const uploaded = await upload; if (uploaded.status === TemporaryFileStatus.SUCCESS) { - this.temporaryFile = { unique: item.unique, file: item.file }; + this.temporaryFile = { temporaryUnique: item.temporaryUnique, file: item.file }; 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 0f4921433d..9fcf08824d 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 @@ -2,7 +2,6 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; 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 = 'success' | 'waiting' | 'error'; @@ -14,7 +13,7 @@ export enum TemporaryFileStatus { export interface UmbTemporaryFileModel { file: File; - unique: string; + temporaryUnique: string; status?: TemporaryFileStatus; } @@ -23,7 +22,7 @@ export class UmbTemporaryFileManager< > extends UmbControllerBase { #temporaryFileRepository; - #queue = new UmbArrayState([], (item) => item.unique); + #queue = new UmbArrayState([], (item) => item.temporaryUnique); public readonly queue = this.#queue.asObservable(); constructor(host: UmbControllerHost) { @@ -66,18 +65,18 @@ export class UmbTemporaryFileManager< if (!queue.length) return filesCompleted; for (const item of queue) { - if (!item.unique) throw new Error(`Unique is missing for item ${item}`); + if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`); - const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file); + const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, 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) { status = TemporaryFileStatus.ERROR; - this.#queue.updateOne(item.unique, { ...item, status }); + this.#queue.updateOne(item.temporaryUnique, { ...item, status }); } else { status = TemporaryFileStatus.SUCCESS; - this.#queue.updateOne(item.unique, { ...item, status }); + this.#queue.updateOne(item.temporaryUnique, { ...item, status }); } filesCompleted.push({ ...item, status }); 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 d5e6900e27..02d889d5a0 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 @@ -56,7 +56,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); - this.#manager?.uploadOne({ unique, file }); + this.#manager?.uploadOne({ temporaryUnique: unique, file }); this.dispatchEvent(new UmbChangeEvent()); } 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 ae6fa4a43f..8786b50d4a 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,3 +1,4 @@ +import type { UmbMediaCardItemModel } from '../../modals/index.js'; 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'; @@ -8,6 +9,8 @@ import type { } 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'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; export class UmbMediaPickerContext extends UmbPickerInputContext< UmbMediaItemModel, @@ -15,7 +18,32 @@ export class UmbMediaPickerContext extends UmbPickerInputContext< UmbMediaTreePickerModalData, UmbMediaTreePickerModalValue > { + #imagingRepository: UmbImagingRepository; + + #cardItems = new UmbArrayState([], (x) => x.unique); + readonly cardItems = this.#cardItems.asObservable(); + constructor(host: UmbControllerHost) { super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL); + this.#imagingRepository = new UmbImagingRepository(host); + + this.observe(this.selectedItems, async (selectedItems) => { + if (!selectedItems.length) return; + const { data } = await this.#imagingRepository.requestResizedItems(selectedItems.map((x) => x.unique)); + + this.#cardItems.setValue( + selectedItems.map((item) => { + const url = data?.find((x) => x.unique === item.unique)?.url; + return { + icon: item.mediaType.icon, + name: item.name, + unique: item.unique, + isTrashed: item.isTrashed, + entityType: item.entityType, + url, + }; + }), + ); + }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index bfcea58eb4..a7129e39b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -1,3 +1,4 @@ +import type { UmbMediaCardItemModel } from '../../modals/index.js'; import type { UmbMediaItemModel } from '../../repository/index.js'; import { UmbMediaPickerContext } from './input-media.context.js'; import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; @@ -7,6 +8,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController, UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbUploadableFileModel } from '@umbraco-cms/backoffice/media'; @customElement('umb-input-media') export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -103,7 +105,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') private _editMediaPath = ''; @state() - private _items?: Array; + private _items?: Array; #pickerContext = new UmbMediaPickerContext(this); @@ -120,7 +122,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') }); this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(','))); - this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.cardItems, (cardItems) => (this._items = cardItems)); this.addValidator( 'rangeUnderflow', @@ -154,8 +156,25 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') }); } + async #onUploadCompleted(e: CustomEvent) { + const completed = e.detail?.completed as Array; + const uploaded = completed.map((file) => file.unique); + + this.selection = [...this.selection, ...uploaded]; + this.dispatchEvent(new UmbChangeEvent()); + } + render() { - return html`
${this.#renderItems()} ${this.#renderAddButton()}
`; + return html`${this.#renderDropzone()} +
${this.#renderItems()} ${this.#renderAddButton()}
`; + } + + #renderDropzone() { + if (this._items && this._items.length >= this.max) return; + return html``; } #renderItems() { @@ -181,13 +200,13 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') `; } - #renderItem(item: UmbMediaItemModel) { + #renderItem(item: UmbMediaCardItemModel) { // TODO: `file-ext` value has been hardcoded here. Find out if API model has value for it. [LK] return html` - + + ${item.url + ? html`${item.name}` + : html``} ${this.#renderIsTrashed(item)} ${this.#renderOpenButton(item)} @@ -204,7 +223,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') `; } - #renderIsTrashed(item: UmbMediaItemModel) { + #renderIsTrashed(item: UmbMediaCardItemModel) { if (!item.isTrashed) return; return html` @@ -213,7 +232,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') `; } - #renderOpenButton(item: UmbMediaItemModel) { + #renderOpenButton(item: UmbMediaCardItemModel) { if (!this.showOpenButton) return; return html` ([], (upload) => upload.unique); + #completed = new UmbArrayState( + [], + (upload) => upload.temporaryUnique, + ); public readonly completed = this.#completed.asObservable(); constructor(host: UmbControllerHost) { @@ -56,7 +58,7 @@ export class UmbDropzoneManager extends UmbControllerBase { const temporaryFiles: Array = []; for (const file of files) { - const uploaded = await this.#tempFileManager.uploadOne({ unique: UmbId.new(), file }); + const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: UmbId.new(), file }); this.#completed.setValue([...this.#completed.getValue(), uploaded]); temporaryFiles.push(uploaded); } @@ -107,7 +109,12 @@ export class UmbDropzoneManager extends UmbControllerBase { // 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 }); + uploadableFiles.push({ + temporaryUnique: UmbId.new(), + file, + mediaTypeUnique: mediaType.unique, + unique: UmbId.new(), + }); } notAllowedFiles.forEach((file) => { @@ -142,6 +149,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // Only one allowed option, upload file using that option. const uploadableFile: UmbUploadableFileModel = { unique: UmbId.new(), + temporaryUnique: UmbId.new(), file, mediaTypeUnique: mediaTypes[0].unique, }; @@ -156,6 +164,7 @@ export class UmbDropzoneManager extends UmbControllerBase { const uploadableFile: UmbUploadableFileModel = { unique: UmbId.new(), + temporaryUnique: UmbId.new(), file, mediaTypeUnique: mediaType.unique, }; @@ -211,6 +220,7 @@ export class UmbDropzoneManager extends UmbControllerBase { if (upload.status === TemporaryFileStatus.SUCCESS) { // Upload successful. Create media item. const preset: Partial = { + unique: file.unique, mediaType: { unique: upload.mediaTypeUnique, collection: null, @@ -227,7 +237,7 @@ export class UmbDropzoneManager extends UmbControllerBase { values: [ { alias: 'umbracoFile', - value: { temporaryFileId: upload.unique }, + value: { temporaryFileId: upload.temporaryUnique }, culture: null, segment: null, }, 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 d88040d01a..35c1551589 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,14 +1,29 @@ -import { UmbDropzoneManager } from './dropzone-manager.class.js'; -import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; +import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js'; +import { 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 { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { @property({ attribute: false }) parentUnique: string | null = null; + @property({ type: Boolean }) + multiple: boolean = true; + + @property({ type: Boolean }) + createAsTemporary: boolean = false; + + //TODO: logic to disable the dropzone? + + #files: Array = []; + + public getFiles() { + return this.#files; + } + public browse() { const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; return element.browse(); @@ -57,20 +72,25 @@ export class UmbDropzoneElement extends UmbLitElement { this.dispatchEvent(new UmbProgressEvent(progress)); if (completed.length === files.length) { - this.dispatchEvent(new UmbChangeEvent()); + this.#files = completed; + this.dispatchEvent(new CustomEvent('change', { detail: { completed } })); dropzoneManager.destroy(); } }, '_observeCompleted', ); //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. - await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + if (this.createAsTemporary) { + await dropzoneManager.createFilesAsTemporary(files); + } else { + await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + } } render() { return html``; 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 c76bd46ae8..ef3ff5241e 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 @@ -67,7 +67,14 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { const url = data?.find((media) => media.unique === item.unique)?.url; - return { name: item.name, unique: item.unique, url, icon: item.mediaType.icon, entityType: item.entityType }; + return { + name: item.name, + unique: item.unique, + url, + icon: item.mediaType.icon, + entityType: item.entityType, + isTrashed: item.isTrashed, + }; }); } 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 768968bff8..04c244d93c 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 @@ -5,8 +5,9 @@ export interface UmbMediaCardItemModel { name: string; unique: string; entityType: UmbMediaEntityType; + isTrashed: boolean; + icon: string; url?: string; - icon?: string; } export interface UmbMediaPathModel extends UmbEntityModel { From b3896d713d215b5767f21f22bfecaabc0b2e41e3 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 13:49:07 +0200 Subject: [PATCH 02/11] use media picker rather than media tree picker --- .../media/components/input-media/input-media.context.ts | 5 ++--- 1 file changed, 2 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 8786b50d4a..bf38680c3d 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,8 +1,7 @@ -import type { UmbMediaCardItemModel } from '../../modals/index.js'; +import { UMB_MEDIA_PICKER_MODAL, type UmbMediaCardItemModel } from '../../modals/index.js'; 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, @@ -24,7 +23,7 @@ export class UmbMediaPickerContext extends UmbPickerInputContext< readonly cardItems = this.#cardItems.asObservable(); constructor(host: UmbControllerHost) { - super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL); + super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_PICKER_MODAL); this.#imagingRepository = new UmbImagingRepository(host); this.observe(this.selectedItems, async (selectedItems) => { From a1dfccc087a576c0f9a76618d2eeafb0bfb3749b Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 14:01:05 +0200 Subject: [PATCH 03/11] fix error --- .../media/components/input-media/input-media.context.ts | 5 ++++- .../media/components/input-media/input-media.element.ts | 6 ++++-- 2 files changed, 8 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 bf38680c3d..2d9b5b5249 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 @@ -27,7 +27,10 @@ export class UmbMediaPickerContext extends UmbPickerInputContext< this.#imagingRepository = new UmbImagingRepository(host); this.observe(this.selectedItems, async (selectedItems) => { - if (!selectedItems.length) return; + if (!selectedItems?.length) { + this.#cardItems.setValue([]); + return; + } const { data } = await this.#imagingRepository.requestResizedItems(selectedItems.map((x) => x.unique)); this.#cardItems.setValue( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index a7129e39b8..1c09a1c0bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -122,7 +122,9 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') }); this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(','))); - this.observe(this.#pickerContext.cardItems, (cardItems) => (this._items = cardItems)); + this.observe(this.#pickerContext.cardItems, (cardItems) => { + this._items = cardItems; + }); this.addValidator( 'rangeUnderflow', @@ -178,7 +180,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') } #renderItems() { - if (!this._items) return; + if (!this._items?.length) return; return html`${repeat( this._items, (item) => item.unique, From 9157f408198e989e050a7f197460d9a6aad208e1 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 14:55:12 +0200 Subject: [PATCH 04/11] Bugfix: ImgCropper remove item --- .../input-image-cropper/input-image-cropper.element.ts | 7 ++++--- .../property-editor-ui-image-cropper.element.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) 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 02d889d5a0..e5dbf38b11 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 @@ -68,8 +68,9 @@ export class UmbInputImageCropperElement extends UmbLitElement { #onRemove = () => { this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null }); - if (!this.fileUnique) return; - this.#manager?.removeOne(this.fileUnique); + if (this.fileUnique) { + this.#manager?.removeOne(this.fileUnique); + } this.fileUnique = undefined; this.file = undefined; @@ -114,7 +115,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { const value = (e.target as UmbInputImageCropperFieldElement).value; if (!value) { - this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 } }; + this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, temporaryFileId: null }; this.dispatchEvent(new UmbChangeEvent()); return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts index f726bbae26..0c546344a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts @@ -15,6 +15,7 @@ import { export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ attribute: false }) value: UmbImageCropperPropertyEditorValue = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, @@ -28,6 +29,7 @@ export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implem if (changedProperties.has('value')) { if (!this.value) { this.value = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, From 2c18b2ba4aa2c14f1232731f620c724e526205d4 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 15:40:58 +0200 Subject: [PATCH 05/11] avoid collision with umbsortercontroller --- .../src/packages/media/media/dropzone/dropzone.element.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 35c1551589..f3f4e8b4ca 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 @@ -43,7 +43,9 @@ export class UmbDropzoneElement extends UmbLitElement { document.removeEventListener('drop', this.#handleDrop.bind(this)); } - #handleDragEnter() { + #handleDragEnter(e: DragEvent) { + // Avoid collision with UmbSorterController + if (!e.dataTransfer?.types?.length) return; this.toggleAttribute('dragging', true); } From a42918cb4fb072623e950a52fc28525b9cb50ab0 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 16 May 2024 15:41:04 +0200 Subject: [PATCH 06/11] better thumbnails --- .../media/media/collection/media-collection.context.ts | 8 ++++++-- .../media/components/input-media/input-media.context.ts | 6 +++++- .../modals/media-picker/media-picker-modal.element.ts | 8 ++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index ee33b64eea..dc58abba08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts @@ -1,9 +1,10 @@ -import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js'; import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< UmbMediaCollectionItemModel, @@ -21,7 +22,10 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< this.observe(this.items, async (items) => { if (!items?.length) return; - const { data } = await this.#imagingRepository.requestResizedItems(items.map((m) => m.unique)); + const { data } = await this.#imagingRepository.requestResizedItems( + items.map((m) => m.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); this.#thumbnailItems.setValue( items.map((item) => { 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 2d9b5b5249..4082e820eb 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 @@ -10,6 +10,7 @@ import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbMediaPickerContext extends UmbPickerInputContext< UmbMediaItemModel, @@ -31,7 +32,10 @@ export class UmbMediaPickerContext extends UmbPickerInputContext< this.#cardItems.setValue([]); return; } - const { data } = await this.#imagingRepository.requestResizedItems(selectedItems.map((x) => x.unique)); + const { data } = await this.#imagingRepository.requestResizedItems( + selectedItems.map((x) => x.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); this.#cardItems.setValue( selectedItems.map((item) => { 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 ef3ff5241e..a3aa80888b 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,3 @@ -import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; @@ -6,8 +5,10 @@ import type { UmbMediaCardItemModel, UmbMediaPathModel } 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 { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE }; @@ -63,7 +64,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement): Promise> { if (!items.length) return []; - const { data } = await this.#imagingRepository.requestResizedItems(items.map((item) => item.unique)); + const { data } = await this.#imagingRepository.requestResizedItems( + items.map((item) => item.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); return items.map((item): UmbMediaCardItemModel => { const url = data?.find((media) => media.unique === item.unique)?.url; From 6c842ea7a1c355813441a6bc9332bb7f975a620a Mon Sep 17 00:00:00 2001 From: JesmoDev <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 16 May 2024 15:42:05 +0200 Subject: [PATCH 07/11] fix preview --- .../input-image-cropper/image-cropper-preview.element.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts index ffda92c71c..7d94c9740e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts @@ -33,11 +33,7 @@ export class UmbImageCropperPreviewElement extends LitElement { if (!this.crop) return; await this.updateComplete; // Wait for the @query to be resolved - - if (!this.imageElement.complete) { - // Wait for the image to load - await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement))); - } + await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement))); const container = this.imageContainerElement.getBoundingClientRect(); const cropAspectRatio = this.crop.width / this.crop.height; From 8df933b42f3d4f9853cc35b2519c5ae7f462a902 Mon Sep 17 00:00:00 2001 From: JesmoDev <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 16 May 2024 16:03:50 +0200 Subject: [PATCH 08/11] fix focus setter --- .../image-cropper-focus-setter.element.ts | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts index e23da648b4..4e98f5f90b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts @@ -5,20 +5,15 @@ import { LitElement, css, html, nothing, customElement, property, query } from ' @customElement('umb-image-cropper-focus-setter') export class UmbImageCropperFocusSetterElement extends LitElement { - @query('#image') imageElement?: HTMLImageElement; + @query('#image') imageElement!: HTMLImageElement; @query('#wrapper') wrapperElement?: HTMLImageElement; - @query('#focal-point') focalPointElement?: HTMLImageElement; + @query('#focal-point') focalPointElement!: HTMLImageElement; @property({ type: String }) src?: string; @property({ attribute: false }) focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 }; #DOT_RADIUS = 6 as const; - connectedCallback() { - super.connectedCallback(); - this.#addEventListeners(); - } - disconnectedCallback() { super.disconnectedCallback(); this.#removeEventListeners(); @@ -33,33 +28,46 @@ export class UmbImageCropperFocusSetterElement extends LitElement { } } + protected update(changedProperties: PropertyValueMap | Map): void { + super.update(changedProperties); + + if (changedProperties.has('src')) { + if (this.src) { + this.#initializeImage(); + } + } + } + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); this.style.setProperty('--dot-radius', `${this.#DOT_RADIUS}px`); + } - if (this.focalPointElement) { - this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`; - this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`; - } - if (this.imageElement) { - this.imageElement.onload = () => { - if (!this.imageElement || !this.wrapperElement) return; - const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight; - const hostRect = this.getBoundingClientRect(); - const image = this.imageElement.getBoundingClientRect(); + async #initializeImage() { + await this.updateComplete; // Wait for the @query to be resolved - if (image.width > hostRect.width) { - this.imageElement.style.width = '100%'; - } - if (image.height > hostRect.height) { - this.imageElement.style.height = '100%'; - } + this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`; + this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`; - this.imageElement.style.aspectRatio = `${imageAspectRatio}`; - this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`; - }; - } + this.imageElement.onload = () => { + if (!this.imageElement || !this.wrapperElement) return; + const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight; + const hostRect = this.getBoundingClientRect(); + const image = this.imageElement.getBoundingClientRect(); + + if (image.width > hostRect.width) { + this.imageElement.style.width = '100%'; + } + if (image.height > hostRect.height) { + this.imageElement.style.height = '100%'; + } + + this.imageElement.style.aspectRatio = `${imageAspectRatio}`; + this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`; + }; + + this.#addEventListeners(); } async #addEventListeners() { From 6e4c665807480c73a3a60d3e59dec7f6111be4b1 Mon Sep 17 00:00:00 2001 From: JesmoDev <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 16 May 2024 16:09:01 +0200 Subject: [PATCH 09/11] remove selection in image cropper --- .../components/input-image-cropper/image-cropper.element.ts | 1 + 1 file changed, 1 insertion(+) 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 6599c9a769..807193942c 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 @@ -376,6 +376,7 @@ export class UmbImageCropperElement extends LitElement { #image { display: block; position: absolute; + user-select: none; } #slider { From bf429d7191f112b1ecb69017bd0b682ad44b6a78 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 17 May 2024 11:23:07 +0200 Subject: [PATCH 10/11] Chore: ImgCard same size icons --- .../input-upload-field/input-upload-field.element.ts | 3 +++ .../views/grid/media-grid-collection-view.element.ts | 6 ++---- .../media/components/input-media/input-media.element.ts | 8 ++++++-- .../src/packages/media/media/dropzone/dropzone.element.ts | 7 +++++-- .../modals/media-picker/media-picker-modal.element.ts | 2 +- 5 files changed, 17 insertions(+), 9 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 6dce37947f..96110d38c2 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 @@ -172,6 +172,9 @@ export class UmbInputUploadFieldElement extends UmbLitElement { static styles = [ css` + :host { + position: relative; + } uui-icon { vertical-align: sub; margin-right: var(--uui-size-space-4); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index d8e8f2d220..b5434aefe9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -116,7 +116,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbMediaCollectionItemModel) { - // TODO: Fix the file extension when media items have a file extension. [?] return html` this.#onOpen(event, item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)} - class="media-item" - file-ext="${item.icon}"> + class="media-item"> ${item.url ? html`${item.name}` : html``} @@ -156,7 +154,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { gap: var(--uui-size-space-5); } umb-icon { - font-size: var(--uui-size-24); + font-size: var(--uui-size-8); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index bba1bda31e..70e3e0c948 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -199,7 +199,6 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') } #renderItem(item: UmbMediaCardItemModel) { - // TODO: `file-ext` value has been hardcoded here. Find out if API model has value for it. [LK] return html` ${item.url @@ -208,10 +207,11 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') ${this.#renderIsTrashed(item)} ${this.#renderOpenButton(item)} - + this.#pickerContext.requestRemoveItem(item.unique)} label="Remove media ${item.name}"> @@ -264,6 +264,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') margin: 0 auto; } + uui-card-media umb-icon { + font-size: var(--uui-size-8); + } + uui-card-media[drag-placeholder] { opacity: 0.2; } 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 f3f4e8b4ca..726347040b 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 @@ -16,6 +16,9 @@ export class UmbDropzoneElement extends UmbLitElement { @property({ type: Boolean }) createAsTemporary: boolean = false; + @property({ type: Array, attribute: false }) + accept: Array = []; + //TODO: logic to disable the dropzone? #files: Array = []; @@ -92,10 +95,10 @@ export class UmbDropzoneElement extends UmbLitElement { render() { return html``; + label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}">`; } static styles = [ 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 a3aa80888b..de83d120a3 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 @@ -247,7 +247,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement Date: Fri, 17 May 2024 11:58:12 +0200 Subject: [PATCH 11/11] hide overflow --- .../input-image-cropper/image-cropper-focus-setter.element.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts index 4e98f5f90b..ce4783482f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts @@ -142,6 +142,7 @@ export class UmbImageCropperFocusSetterElement extends LitElement { } /* Wrapper is used to make the focal point position responsive to the image size */ #wrapper { + overflow: hidden; position: relative; display: flex; margin: auto;