From d5a021d7d47e390e05738573b36cb460f350e231 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:06:49 +0100 Subject: [PATCH 01/43] feat: shows notification when no suitable media type is found --- .../media/media/dropzone/dropzone-manager.class.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 839d4c39d35a..6d48da923f9a 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 @@ -19,6 +19,7 @@ import { UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-t import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; /** * Manages the dropzone and uploads folders and files to the server. @@ -48,9 +49,15 @@ export class UmbDropzoneManager extends UmbControllerBase { readonly #progressItems = new UmbArrayState([], (x) => x.unique); public readonly progressItems = this.#progressItems.asObservable(); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + constructor(host: UmbControllerHost) { super(host); this.#host = host; + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); } public setIsFoldersAllowed(isAllowed: boolean) { @@ -134,6 +141,11 @@ export class UmbDropzoneManager extends UmbControllerBase { async #createOneMediaItem(item: UmbUploadableItem) { const options = await this.#getMediaTypeOptions(item); if (!options.length) { + this.#notificationContext?.peek('warning', { + data: { + message: `No media types are allowed for ${item.temporaryFile?.file.name}.`, + }, + }); return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); } From 455992a9d1020ad70449f91ad08a558e078a6e70 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:07:41 +0100 Subject: [PATCH 02/43] chore: rearrange imports --- .../src/packages/media/media/dropzone/dropzone.element.ts | 2 +- 1 file changed, 1 insertion(+), 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 adfcf191f475..f90199879d40 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,9 +1,9 @@ import { UmbDropzoneManager } from './dropzone-manager.class.js'; +import { UmbDropzoneSubmittedEvent } from './dropzone-submitted.event.js'; import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from './types.js'; import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbDropzoneSubmittedEvent } from './dropzone-submitted.event.js'; @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { From 4f6b4b73e5d884abef89bfcc693cd8881890d462 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:07:52 +0100 Subject: [PATCH 03/43] feat: use a forward ref to find the dropzone --- .../media/collection/media-collection.element.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 31a89e174465..d8f1b6945e0e 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 @@ -3,7 +3,7 @@ import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../workspace/media-workspace.contex import type { UmbDropzoneSubmittedEvent } from '../dropzone/dropzone-submitted.event.js'; import type { UmbDropzoneElement } from '../dropzone/dropzone.element.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; -import { customElement, html, query, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, ref, state, when, type Ref } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; @@ -18,9 +18,6 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { @state() private _unique: string | null = null; - @query('#dropzone') - private _dropzone!: UmbDropzoneElement; - constructor() { super(); @@ -35,9 +32,10 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { }); } - #observeProgressItems() { + #observeProgressItems(dropzone?: Element) { + if (!dropzone) return; this.observe( - this._dropzone.progressItems(), + (dropzone as UmbDropzoneElement).progressItems(), (progressItems) => { progressItems.forEach((item) => { // We do not update folders as it may have children still being uploaded. @@ -57,7 +55,6 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { .map((p) => ({ unique: p.unique, status: p.status, name: p.temporaryFile?.file.name ?? p.folder?.name })); this.#collectionContext?.setPlaceholders(placeholders); - this.#observeProgressItems(); } async #onComplete(event: Event) { @@ -89,6 +86,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { ${when(this._progress >= 0, () => html``)} Date: Tue, 28 Jan 2025 11:08:09 +0100 Subject: [PATCH 04/43] chore: rearrange imports --- .../packages/media/media/collection/media-collection.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d8f1b6945e0e..cd0081de34ff 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 @@ -3,7 +3,7 @@ import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../workspace/media-workspace.contex import type { UmbDropzoneSubmittedEvent } from '../dropzone/dropzone-submitted.event.js'; import type { UmbDropzoneElement } from '../dropzone/dropzone.element.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; -import { customElement, html, ref, state, when, type Ref } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, ref, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; From 11976a03c63b10f5f66fc45a2aa17a07135de814 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:20:53 +0100 Subject: [PATCH 05/43] chore(mock): send back correct header --- .../handlers/temporary-file/temporary-file.handlers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts index 07f3e8bcef0d..0cd260c77da5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts @@ -7,6 +7,12 @@ const UMB_SLUG = 'temporary-file'; export const handlers = [ rest.post(umbracoPath(`/${UMB_SLUG}`), async (_req, res, ctx) => { - return res(ctx.delay(), ctx.status(201), ctx.text(UmbId.new())); + const guid = UmbId.new(); + return res( + ctx.delay(), + ctx.status(201), + ctx.set('Umb-Generated-Resource', guid), + ctx.text(guid), + ); }), ]; From 37b3c9474d85a1b56f2c83e8399651e8dc7b7c21 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:21:32 +0100 Subject: [PATCH 06/43] feat: avoid using the context consumer to get a token, but instead mimick the OpenAPI generator --- .../core/resources/tryXhrRequest.function.ts | 15 ++++----------- .../temporary-file.server.data-source.ts | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts index 33b524e6966d..749fafc7296e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts @@ -1,23 +1,16 @@ -import { UMB_AUTH_CONTEXT } from '../auth/auth.context.token.js'; import type { XhrRequestOptions } from './types.js'; import { UmbResourceController } from './resource.controller.js'; -import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { OpenAPI, type CancelablePromise } from '@umbraco-cms/backoffice/external/backend-api'; /** * Make an XHR request. - * @param host The controller host for this controller to be appended to. - * @param options The options for the XHR request. + * @param {XhrRequestOptions} options The options for the XHR request. + * @returns {CancelablePromise} A promise that can be cancelled. */ -export function tryXhrRequest(host: UmbControllerHost, options: XhrRequestOptions): CancelablePromise { +export function tryXhrRequest(options: XhrRequestOptions): CancelablePromise { return UmbResourceController.xhrRequest({ ...options, baseUrl: OpenAPI.BASE, - async token() { - const contextConsumer = new UmbContextConsumerController(host, UMB_AUTH_CONTEXT).asPromise(); - const authContext = await contextConsumer; - return authContext.getLatestToken(); - }, + token: OpenAPI.TOKEN as never, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts index 29f33653da75..644eaa7dcdab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts @@ -35,7 +35,7 @@ export class UmbTemporaryFileServerDataSource { const body = new FormData(); body.append('Id', id); body.append('File', file); - const xhrRequest = tryXhrRequest(this.#host, { + const xhrRequest = tryXhrRequest({ url: '/umbraco/management/api/v1/temporary-file', method: 'POST', responseHeader: 'Umb-Generated-Resource', From badf18f514c2c3536ad5fdc67481e45e7f9c4d1f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:21:48 +0100 Subject: [PATCH 07/43] chore(mock): allow more file types --- .../src/mocks/data/data-type/data-type.data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 269e56f6271f..54263d68b1ec 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -722,7 +722,7 @@ export const data: Array = [ values: [ { alias: 'fileExtensions', - value: ['jpg', 'jpeg', 'png', 'pdf'], + value: ['jpg', 'jpeg', 'png', 'pdf', 'mov', 'iso'], }, { alias: 'multiple', From a79aa2c5fd8feb9ef70cda018c8d609a1e3b6efe Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:38:27 +0100 Subject: [PATCH 08/43] chore(mock): create more upload fields --- .../mocks/data/data-type/data-type.data.ts | 65 ++++++++++++++++++- .../mocks/data/media-type/media-type.data.ts | 16 ++--- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 54263d68b1ec..119c0cc6e2e5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -722,7 +722,70 @@ export const data: Array = [ values: [ { alias: 'fileExtensions', - value: ['jpg', 'jpeg', 'png', 'pdf', 'mov', 'iso'], + value: ['jpg', 'jpeg', 'png', 'svg'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Files)', + id: 'dt-uploadFieldFiles', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['pdf', 'iso'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Movies)', + id: 'dt-uploadFieldMovies', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['mp4', 'mov'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Vector)', + id: 'dt-uploadFieldVector', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['svg'], }, { alias: 'multiple', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts index 34688601f76f..1614c94fc1ca 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts @@ -15,7 +15,7 @@ export type UmbMockMediaTypeUnionModel = export const data: Array = [ { - name: 'Media Type 1', + name: 'Image', id: 'media-type-1-id', parent: null, description: 'Media type 1 description', @@ -105,7 +105,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 2', + name: 'Audio', id: 'media-type-2-id', parent: null, description: 'Media type 2 description', @@ -118,7 +118,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldFiles' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -155,7 +155,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 3', + name: 'Vector Graphics', id: 'media-type-3-id', parent: null, description: 'Media type 3 description', @@ -168,7 +168,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldVector' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -205,7 +205,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 4', + name: 'Movie', id: 'media-type-4-id', parent: null, description: 'Media type 4 description', @@ -218,7 +218,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldMovies' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -268,7 +268,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldFiles' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, From 61abee47495095a441da3464f39a4744696e6c01 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:38:39 +0100 Subject: [PATCH 09/43] chore(mock): also look for mediaPicker fields --- .../src/mocks/data/media-type/media-type.db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts index 951f9cf26093..6f271a453df0 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts @@ -52,7 +52,7 @@ class UmbMediaTypeMockDB extends UmbEntityMockDbBase { const allowedTypes = this.data.filter((field) => { const allProperties = field.properties.flat(); - const fileUploadType = allProperties.find((prop) => prop.alias === 'umbracoFile'); + const fileUploadType = allProperties.find((prop) => prop.alias === 'umbracoFile' || prop.alias === 'mediaPicker'); if (!fileUploadType) return false; const dataType = umbDataTypeMockDb.read(fileUploadType.dataType.id); From 4990a8446550b6129edc7bd0af67810d3beceb52 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:02:00 +0100 Subject: [PATCH 10/43] chore(mock): improve media mock db --- .../src/mocks/data/media/media.data.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts index 01d0cdb071e6..86eb0740dbf8 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts @@ -16,19 +16,23 @@ export const data: Array = [ isTrashed: false, mediaType: { id: 'media-type-1-id', - icon: 'icon-bug', + icon: 'icon-picture', }, values: [ + { + editorAlias: 'Umbraco.UploadField', + alias: 'dt-uploadField', + }, { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaHeadline', + alias: 'mediaType1Property1', value: 'The daily life at Umbraco HQ', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Flipped Car', createDate: '2023-02-06T15:31:46.876902', @@ -51,14 +55,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Umbracoffee', createDate: '2023-02-06T15:31:46.876902', @@ -83,7 +87,7 @@ export const data: Array = [ variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'People', createDate: '2023-02-06T15:31:46.876902', @@ -108,7 +112,7 @@ export const data: Array = [ variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'John Smith', createDate: '2023-02-06T15:31:46.876902', @@ -131,14 +135,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Jane Doe', createDate: '2023-02-06T15:31:46.876902', @@ -161,14 +165,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'John Doe', createDate: '2023-02-06T15:31:46.876902', @@ -191,14 +195,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'A very nice hat', createDate: '2023-02-06T15:31:46.876902', @@ -221,14 +225,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Fancy old chair', createDate: '2023-02-06T15:31:46.876902', From 9f821e5ec6da6457f0480fbaff9e6229dcf1bd22 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:02:13 +0100 Subject: [PATCH 11/43] chore(mock): add missing endpoints --- .../mocks/handlers/media/detail.handlers.ts | 18 ++++++++++++++++++ .../src/mocks/handlers/media/tree.handlers.ts | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts index 96cb5a1ffcfd..fe52ac9a25fe 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts @@ -8,6 +8,7 @@ import type { UpdateMediaRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import type { UmbMediaDetailModel } from '@umbraco-cms/backoffice/media'; export const detailHandlers = [ rest.post(umbracoPath(`${UMB_SLUG}`), async (req, res, ctx) => { @@ -44,6 +45,23 @@ export const detailHandlers = [ return res(ctx.status(200), ctx.json(response)); }), + rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return res(ctx.status(400)); + const model = await req.json(); + if (!model) return res(ctx.status(400)); + + const hasMediaPickerOrFileUploadValue = model.values.some((v) => { + return v.editorAlias === 'Umbraco.UploadField' && v.value; + }); + + if (!hasMediaPickerOrFileUploadValue) { + return res(ctx.status(400, 'No media picker or file upload value found')); + } + + return res(ctx.status(200)); + }), + rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts index 308c04be5274..53e112cba419 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts @@ -19,4 +19,11 @@ export const treeHandlers = [ const response = umbMediaMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), + + rest.get(umbracoPath(`/tree${UMB_SLUG}/ancestors`), (req, res, ctx) => { + const descendantId = req.url.searchParams.get('descendantId'); + if (!descendantId) return; + const response = umbMediaMockDb.tree.getAncestorsOf({ descendantId }); + return res(ctx.status(200), ctx.json(response)); + }), ]; From 38720c088b7b6a55e1c65cf7e7ee46130d3d78ec Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:02:15 +0100 Subject: [PATCH 12/43] chore(mock): update media data --- src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts index 86eb0740dbf8..dc4e3507dd1c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts @@ -21,7 +21,10 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.UploadField', - alias: 'dt-uploadField', + alias: 'mediaPicker', + value: { + src: '/umbraco/backoffice/assets/installer-illustration.svg', + }, }, { editorAlias: 'Umbraco.TextBox', From 9addb3e7fb8b2a6ae76ef2f00c525943b3825130 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:30:57 +0100 Subject: [PATCH 13/43] chore(mock): fix aliases for media grid and table --- .../src/mocks/data/data-type/data-type.data.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 119c0cc6e2e5..0e8b63935a5e 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -975,8 +975,16 @@ export const data: Array = [ { alias: 'layouts', value: [ - { icon: 'icon-grid', isSystem: true, name: 'Grid', path: '', selected: true }, - { icon: 'icon-list', isSystem: true, name: 'Table', path: '', selected: true }, + { + icon: 'icon-grid', + name: 'Media Grid Collection View', + collectionView: 'Umb.CollectionView.Media.Grid', + }, + { + icon: 'icon-list', + name: 'Media Table Collection View', + collectionView: 'Umb.CollectionView.Media.Table', + }, ], }, { alias: 'icon', value: 'icon-layers' }, From 7cf4d59b48fd93213cbf8497c3d0ad98d165fa2a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:48:20 +0100 Subject: [PATCH 14/43] chore(mock): add urls to media --- .../src/mocks/data/media/media.data.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts index dc4e3507dd1c..f3a1ad9f9bd5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts @@ -42,7 +42,12 @@ export const data: Array = [ updateDate: '2023-02-06T15:31:51.354764', }, ], - urls: [], + urls: [ + { + culture: null, + url: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + ], }, { hasChildren: false, From dda7b798d7caf1391b7411a82a086621b068561f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:48:33 +0100 Subject: [PATCH 15/43] chore(mock): adds missing endpoint for imaging --- .../mocks/handlers/media/imaging.handlers.ts | 24 +++++++++++++++++++ .../src/mocks/handlers/media/index.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts new file mode 100644 index 000000000000..79ed72a98e22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts @@ -0,0 +1,24 @@ +const { rest } = window.MockServiceWorker; +import { umbMediaMockDb } from '../../data/media/media.db.js'; +import type { GetImagingResizeUrlsResponse } from '@umbraco-cms/backoffice/external/backend-api'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const imagingHandlers = [ + rest.get(umbracoPath('/imaging/resize/urls'), (req, res, ctx) => { + const ids = req.url.searchParams.getAll('id'); + if (!ids) return res(ctx.status(404)); + + const media = umbMediaMockDb.getAll().filter((item) => ids.includes(item.id)); + + const response: GetImagingResizeUrlsResponse = media.map((item) => ({ + id: item.id, + urlInfos: item.urls, + })); + + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json(response), + ); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts index e8034da84d40..3c5545f5f71b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts @@ -3,6 +3,7 @@ import { treeHandlers } from './tree.handlers.js'; import { itemHandlers } from './item.handlers.js'; import { detailHandlers } from './detail.handlers.js'; import { collectionHandlers } from './collection.handlers.js'; +import { imagingHandlers } from './imaging.handlers.js'; export const handlers = [ ...recycleBinHandlers, @@ -10,4 +11,5 @@ export const handlers = [ ...itemHandlers, ...detailHandlers, ...collectionHandlers, + ...imagingHandlers, ]; From a12ec271cd236ed580dd5d73a49a3273d29ac701 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:15:36 +0100 Subject: [PATCH 16/43] fix: reverse order of properties to overwrite existing status --- .../core/temporary-file/temporary-file-manager.class.ts | 2 +- 1 file changed, 1 insertion(+), 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 92c10f37fab7..0651bd58c4ad 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 @@ -34,7 +34,7 @@ export class UmbTemporaryFileManager< ): Promise> { this.#queue.setValue([]); - const items = queueItems.map((item): UploadableItem => ({ status: TemporaryFileStatus.WAITING, ...item })); + const items = queueItems.map((item): UploadableItem => ({ ...item, status: TemporaryFileStatus.WAITING })); this.#queue.append(items); return this.#handleQueue({ ...options }); } From ddbe41586650daaed68d1e7fe9ba5db91dc0e0c2 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:16:25 +0100 Subject: [PATCH 17/43] feat: listen to progress updates on upload and update the `progress` property --- .../temporary-file-manager.class.ts | 5 +++- .../src/packages/core/temporary-file/types.ts | 1 + .../media/dropzone/dropzone-manager.class.ts | 25 ++++++++++++------- .../packages/media/media/dropzone/types.ts | 1 + 4 files changed, 22 insertions(+), 10 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 0651bd58c4ad..eecb4e62b2d3 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 @@ -74,7 +74,10 @@ export class UmbTemporaryFileManager< async #handleUpload(item: UploadableItem) { if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`); - const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file); + const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file, (evt) => { + // Update progress in percent if a callback is provided + if (item.onProgress) item.onProgress((evt.loaded / evt.total) * 100); + }); const status = error ? TemporaryFileStatus.ERROR : TemporaryFileStatus.SUCCESS; this.#queue.updateOne(item.temporaryUnique, { ...item, status }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts index 2c28850f47d3..e9c1697ab8e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts @@ -8,6 +8,7 @@ export interface UmbTemporaryFileModel { file: File; temporaryUnique: string; status?: TemporaryFileStatus; + onProgress?: (progress: number) => void; } export type UmbQueueHandlerCallback = (item: TItem) => Promise; 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 6d48da923f9a..17b5d23e61e2 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 @@ -146,13 +146,13 @@ export class UmbDropzoneManager extends UmbControllerBase { message: `No media types are allowed for ${item.temporaryFile?.file.name}.`, }, }); - return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + return this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); } const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique; if (!mediaTypeUnique) { - return this.#updateProgress(item, UmbFileDropzoneItemStatus.CANCELLED); + return this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED); } if (item.temporaryFile) { @@ -166,7 +166,7 @@ export class UmbDropzoneManager extends UmbControllerBase { for (const item of uploadableItems) { const options = await this.#getMediaTypeOptions(item); if (!options.length) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); continue; } @@ -189,7 +189,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // Upload the file as a temporary file and update progress. const temporaryFile = await this.#uploadAsTemporaryFile(item); if (temporaryFile.status !== TemporaryFileStatus.SUCCESS) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); return; } @@ -198,9 +198,9 @@ export class UmbDropzoneManager extends UmbControllerBase { const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); if (data) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } } @@ -208,9 +208,9 @@ export class UmbDropzoneManager extends UmbControllerBase { const scaffold = await this.#getItemScaffold(item, mediaTypeUnique); const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); if (data) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } } @@ -218,6 +218,7 @@ export class UmbDropzoneManager extends UmbControllerBase { return this.#tempFileManager.uploadOne({ temporaryUnique: item.temporaryFile.temporaryUnique, file: item.temporaryFile.file, + onProgress: (progress) => this.#updateProgress(item, progress), }); } @@ -304,12 +305,16 @@ export class UmbDropzoneManager extends UmbControllerBase { return uploadableItems; } - #updateProgress(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) { + #updateStatus(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) { this.#progressItems.updateOne(item.unique, { status }); const progress = this.#progress.getValue(); this.#progress.update({ completed: progress.completed + 1 }); } + #updateProgress(item: UmbUploadableItem, progress: number) { + this.#progressItems.updateOne(item.unique, { progress }); + } + readonly #prepareItemsAsUploadable = ( { folders, files }: UmbFileDropzoneDroppedItems, parentUnique: string | null, @@ -321,6 +326,7 @@ export class UmbDropzoneManager extends UmbControllerBase { unique: UmbId.new(), parentUnique, status: UmbFileDropzoneItemStatus.WAITING, + progress: 0, temporaryFile: { file, temporaryUnique: UmbId.new() }, }); } @@ -331,6 +337,7 @@ export class UmbDropzoneManager extends UmbControllerBase { unique, parentUnique, status: UmbFileDropzoneItemStatus.WAITING, + progress: 100, // Folders are created instantly. folder: { name: subfolder.folderName }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts index 0e99dbb2d709..f1b90a32e885 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts @@ -11,6 +11,7 @@ export interface UmbUploadableItem { unique: string; parentUnique: string | null; status: UmbFileDropzoneItemStatus; + progress: number; folder?: { name: string }; temporaryFile?: UmbTemporaryFileModel; } From 900f57c648cb3ff78025761e10a0ec33d0731221 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:16:56 +0100 Subject: [PATCH 18/43] feat: adds tracking of upload progress to placeholders --- .../media/media/collection/media-collection.context.ts | 6 ++++++ .../media/media/collection/media-collection.element.ts | 1 + .../src/packages/media/media/collection/types.ts | 1 + 3 files changed, 8 insertions(+) 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 ff7cf3939db9..e1ded4959298 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 @@ -33,6 +33,7 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< updateDate: date, createDate: date, entityType: UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE, + progress: 0, ...placeholder, })) .reverse(); @@ -48,6 +49,11 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< this.#placeholders.updateOne(unique, { status }); } + updatePlaceholderProgress(unique: string, progress: number) { + this._items.updateOne(unique, { progress }); + this.#placeholders.updateOne(unique, { progress }); + } + /** * Requests the collection from the repository. * @returns {Promise} 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 cd0081de34ff..e5deb42fa9c0 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 @@ -42,6 +42,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { if (item.folder?.name) return; this.#collectionContext?.updatePlaceholderStatus(item.unique, item.status); + this.#collectionContext?.updatePlaceholderProgress(item.unique, item.progress); }); }, '_observeProgressItems', 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 1a18e4ba71e0..13a2418271f2 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 @@ -23,6 +23,7 @@ export interface UmbMediaCollectionItemModel { values?: Array<{ alias: string; value: string }>; url?: string; status?: UmbFileDropzoneItemStatus; + progress: number; } export interface UmbEditableMediaCollectionItemModel { From 647232a416eb8b07992e46d8911ffcb5b94e959d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:18:20 +0100 Subject: [PATCH 19/43] feat: bind the progress number up on the temporary file badge to indicate upload status --- .../views/grid/media-grid-collection-view.element.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 77fb522a72ea..91a8ca289012 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 @@ -115,7 +115,10 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { const complete = item.status === UmbFileDropzoneItemStatus.COMPLETE; const error = item.status === UmbFileDropzoneItemStatus.ERROR; return html` - + `; } From c0d67e52147a6a9925cd6569a4d49912abf6f6e0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:01:36 +0100 Subject: [PATCH 20/43] feat: optimises progress calculation and makes the badge bigger to be able to show the progress in percent --- .../temporary-file-badge.element.ts | 65 +++++++------------ 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts index be3c318bc993..5c386e72396e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts @@ -4,19 +4,15 @@ import { clamp } from '@umbraco-cms/backoffice/utils'; @customElement('umb-temporary-file-badge') export class UmbTemporaryFileBadgeElement extends UmbLitElement { - private _progress = 0; + #progress = 0; @property({ type: Number }) public set progress(v: number) { - const oldVal = this._progress; - - const p = clamp(v, 0, 100); - this._progress = p; - - this.requestUpdate('progress', oldVal); + const p = clamp(Math.ceil(v), 0, 100); + this.#progress = p; } public get progress(): number { - return this._progress; + return this.#progress; } @property({ type: Boolean, reflect: true }) @@ -26,12 +22,10 @@ export class UmbTemporaryFileBadgeElement extends UmbLitElement { public error = false; override render() { - return html` -
- - ${this.#renderIcon()} -
-
`; + return html`
+ +
${this.#renderIcon()}
+
`; } #renderIcon() { @@ -43,50 +37,39 @@ export class UmbTemporaryFileBadgeElement extends UmbLitElement { return html``; } - return html``; + return `${this.progress}%`; } static override readonly styles = css` - :host { - display: block; - } - #wrapper { - box-sizing: border-box; - box-shadow: inset 0px 0px 0px 6px var(--uui-color-surface); - background-color: var(--uui-color-selected); position: relative; - border-radius: 100%; - font-size: var(--uui-size-6); + height: 75%; } - :host([complete]) #wrapper { - background-color: var(--uui-color-positive); + :host([complete]) { + uui-loader-circle, + #icon { + color: var(--uui-color-positive); + } } - :host([complete]) uui-loader-circle { - color: var(--uui-color-positive); - } - :host([error]) #wrapper { - background-color: var(--uui-color-danger); - } - :host([error]) uui-loader-circle { - color: var(--uui-color-danger); + :host([error]) { + uui-loader-circle, + #icon { + color: var(--uui-color-danger); + } } uui-loader-circle { - display: absolute; z-index: 2; inset: 0; color: var(--uui-color-focus); font-size: var(--uui-size-12); + width: 100%; + height: 100%; } - uui-badge { - padding: 0; - background-color: transparent; - } - - uui-icon { + #icon { + color: var(--uui-color-text); font-size: var(--uui-size-6); position: absolute; top: 50%; From f57d1ae87b8dd3e7f64c3bbafe4ed9403e1a6973 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:17:31 +0100 Subject: [PATCH 21/43] feat: allow text to be normal --- .../views/grid/media-grid-collection-view.element.ts | 4 ---- 1 file changed, 4 deletions(-) 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 91a8ca289012..e62f3ec2b883 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 @@ -136,10 +136,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { align-items: center; } - .media-placeholder-item { - font-style: italic; - } - /** TODO: Remove this fix when UUI gets upgrade to 1.3 */ umb-imaging-thumbnail { pointer-events: none; From e1e5408daf1cb9fb680dd5d1f80d6ce829c2b60b Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:58:34 +0100 Subject: [PATCH 22/43] chore: use correct localization --- .../packages/media/media/dropzone/dropzone-manager.class.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 17b5d23e61e2..7733679150e8 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 @@ -20,6 +20,7 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; /** * Manages the dropzone and uploads folders and files to the server. @@ -50,6 +51,7 @@ export class UmbDropzoneManager extends UmbControllerBase { public readonly progressItems = this.#progressItems.asObservable(); #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #localization = new UmbLocalizationController(this); constructor(host: UmbControllerHost) { super(host); @@ -143,7 +145,7 @@ export class UmbDropzoneManager extends UmbControllerBase { if (!options.length) { this.#notificationContext?.peek('warning', { data: { - message: `No media types are allowed for ${item.temporaryFile?.file.name}.`, + message: `${this.#localization.term('media_disallowedFileType')}: ${item.temporaryFile?.file.name}.`, }, }); return this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); From f8a979162d657ad5ab2159a6dd4ff72e97ccd403 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:58:46 +0100 Subject: [PATCH 23/43] feat: shows error status for anything that isn't waiting or complete --- .../collection/views/grid/media-grid-collection-view.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e62f3ec2b883..f196150457f7 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 @@ -113,7 +113,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { #renderPlaceholder(item: UmbMediaCollectionItemModel) { const complete = item.status === UmbFileDropzoneItemStatus.COMPLETE; - const error = item.status === UmbFileDropzoneItemStatus.ERROR; + const error = item.status !== UmbFileDropzoneItemStatus.WAITING && !complete; return html` Date: Tue, 28 Jan 2025 18:06:33 +0100 Subject: [PATCH 24/43] feat: makes `progress` optional --- .../media/media/collection/media-collection.context.ts | 1 - .../src/packages/media/media/collection/types.ts | 5 ++++- .../views/grid/media-grid-collection-view.element.ts | 2 +- 3 files changed, 5 insertions(+), 3 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 e1ded4959298..8353b3579e1d 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 @@ -33,7 +33,6 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< updateDate: date, createDate: date, entityType: UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE, - progress: 0, ...placeholder, })) .reverse(); 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 13a2418271f2..78d0b0a566c2 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 @@ -23,7 +23,10 @@ export interface UmbMediaCollectionItemModel { values?: Array<{ alias: string; value: string }>; url?: string; status?: UmbFileDropzoneItemStatus; - progress: number; + /** + * The progress of the item in percentage. + */ + progress?: number; } export interface UmbEditableMediaCollectionItemModel { 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 f196150457f7..3a5e187522b2 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,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { const error = item.status !== UmbFileDropzoneItemStatus.WAITING && !complete; return html` `; From fa75e676811695bfc93ea1017d7939eb9d3a3a73 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:46:47 +0100 Subject: [PATCH 25/43] feat: adds repository+store for temporary file configuration --- .../src/packages/core/manifests.ts | 2 + .../config/config.repository.ts | 69 +++++++++++++++++++ .../config/config.server.data-source.ts | 18 +++++ .../config/config.store.token.ts | 7 ++ .../temporary-file/config/config.store.ts | 12 ++++ .../core/temporary-file/config/constants.ts | 2 + .../core/temporary-file/config/index.ts | 4 ++ .../core/temporary-file/config/manifests.ts | 16 +++++ .../src/packages/core/temporary-file/index.ts | 1 + .../packages/core/temporary-file/manifests.ts | 3 + .../src/packages/core/temporary-file/types.ts | 4 ++ 11 files changed, 138 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts index 25e388e0630e..60f4a53b2852 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -19,6 +19,7 @@ import { manifests as propertyTypeManifests } from './property-type/manifests.js import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as sectionManifests } from './section/manifests.js'; import { manifests as serverFileSystemManifests } from './server-file-system/manifests.js'; +import { manifests as temporaryFileManifests } from './temporary-file/manifests.js'; import { manifests as themeManifests } from './themes/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -47,6 +48,7 @@ export const manifests: Array = ...recycleBinManifests, ...sectionManifests, ...serverFileSystemManifests, + ...temporaryFileManifests, ...themeManifests, ...treeManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts new file mode 100644 index 000000000000..577f31f00f28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts @@ -0,0 +1,69 @@ +import type { UmbTemporaryFileConfigurationModel } from '../types.js'; +import { UmbTemporaryFileConfigServerDataSource } from './config.server.data-source.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT } from './config.store.token.js'; +import { UMB_TEMPORARY_FILE_REPOSITORY_ALIAS } from './constants.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implements UmbApi { + /** + * Promise that resolves when the repository has been initialized, i.e. when the configuration has been fetched from the server. + */ + initialized: Promise; + + #dataStore?: typeof UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT.TYPE; + #dataSource = new UmbTemporaryFileConfigServerDataSource(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_TEMPORARY_FILE_REPOSITORY_ALIAS.toString()); + this.initialized = new Promise((resolve) => { + this.consumeContext(UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT, async (store) => { + this.#dataStore = store; + await this.#init(); + resolve(); + }); + }); + } + + async #init() { + // Check if the store already has data + if (this.#dataStore?.getState()) { + return; + } + + const { data } = await this.#dataSource.getConfig(); + + if (data) { + this.#dataStore?.update(data); + } + } + + /** + * Subscribe to the entire configuration. + */ + all() { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.all(); + } + + /** + * Subscribe to a part of the configuration. + * @param part + */ + part( + part: Part, + ): Observable { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.part(part); + } +} + +export default UmbTemporaryFileConfigRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts new file mode 100644 index 000000000000..10d2db7a04e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts @@ -0,0 +1,18 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { TemporaryFileService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbTemporaryFileConfigServerDataSource { + #host; + + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Get the temporary file configuration. + */ + getConfig() { + return tryExecuteAndNotify(this.#host, TemporaryFileService.getTemporaryFileConfiguration()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts new file mode 100644 index 000000000000..04155b61d74f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts @@ -0,0 +1,7 @@ +import type { UmbTemporaryFileConfigStore } from './config.store.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS } from './constants.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT = new UmbContextToken( + UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts new file mode 100644 index 000000000000..18efd1d3c235 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts @@ -0,0 +1,12 @@ +import type { UmbTemporaryFileConfigurationModel } from '../types.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT } from './config.store.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbStoreObjectBase } from '@umbraco-cms/backoffice/store'; + +export class UmbTemporaryFileConfigStore extends UmbStoreObjectBase { + constructor(host: UmbControllerHost) { + super(host, UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT.toString()); + } +} + +export default UmbTemporaryFileConfigStore; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts new file mode 100644 index 000000000000..6f4a206bc0e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts @@ -0,0 +1,2 @@ +export const UMB_TEMPORARY_FILE_REPOSITORY_ALIAS = 'Umb.Repository.TemporaryFile.Config'; +export const UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS = 'UmbTemporaryFileConfigStore'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts new file mode 100644 index 000000000000..3b4d98c12ed5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts @@ -0,0 +1,4 @@ +export * from './config.repository.js'; +export * from './config.store.token.js'; +export * from './config.store.js'; +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts new file mode 100644 index 000000000000..9b5af26e658e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts @@ -0,0 +1,16 @@ +import { UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, UMB_TEMPORARY_FILE_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'store', + alias: UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, + name: 'Temporary File Config Store', + api: () => import('./config.store.js'), + }, + { + type: 'repository', + alias: UMB_TEMPORARY_FILE_REPOSITORY_ALIAS, + name: 'Temporary File Config Repository', + api: () => import('./config.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts index 0a663e6dd2c1..888e6d64b08d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts @@ -1,4 +1,5 @@ export * from './temporary-file.repository.js'; export * from './components/temporary-file-badge.element.js'; +export * from './config/index.js'; export * from './temporary-file-manager.class.js'; export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts new file mode 100644 index 000000000000..ec7e1c29c5de --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as configManifests } from './config/manifests.js'; + +export const manifests = [...configManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts index e9c1697ab8e9..9d417c4bf485 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts @@ -1,3 +1,5 @@ +import type { TemporaryFileConfigurationResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + export enum TemporaryFileStatus { SUCCESS = 'success', WAITING = 'waiting', @@ -17,3 +19,5 @@ export type UmbUploadOptions = { chunkSize?: number; callback?: UmbQueueHandlerCallback; }; + +export type UmbTemporaryFileConfigurationModel = TemporaryFileConfigurationResponseModel; From 207d681630117afa3752af2f83ab34e44c48f4bd Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:47:05 +0100 Subject: [PATCH 26/43] chore(mock): adds mock endpoint for temporary file configuration --- .../temporary-file/temporary-file.handlers.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts index 0cd260c77da5..4aa3391a5af1 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts @@ -1,6 +1,9 @@ const { rest } = window.MockServiceWorker; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; -import type { PostTemporaryFileResponse } from '@umbraco-cms/backoffice/external/backend-api'; +import type { + GetTemporaryFileConfigurationResponse, + PostTemporaryFileResponse, +} from '@umbraco-cms/backoffice/external/backend-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; const UMB_SLUG = 'temporary-file'; @@ -15,4 +18,16 @@ export const handlers = [ ctx.text(guid), ); }), + + rest.get(umbracoPath(`/${UMB_SLUG}/configuration`), async (_req, res, ctx) => { + return res( + ctx.delay(), + ctx.json({ + allowedUploadedFileExtensions: [], + disallowedUploadedFilesExtensions: ['exe', 'dll', 'bat', 'msi'], + maxFileSize: 50000, + imageFileTypes: [], + }), + ); + }), ]; From b1a61250ebc13a6e35c4a12a9181f047e7f6fece Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:45:32 +0100 Subject: [PATCH 27/43] feat: set progress for createTemporaryFiles --- .../media/media/dropzone/dropzone-manager.class.ts | 8 +++----- 1 file changed, 3 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 7733679150e8..4a1bc9e3c610 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 @@ -114,16 +114,14 @@ export class UmbDropzoneManager extends UmbControllerBase { const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: item.temporaryFile.temporaryUnique, file: item.temporaryFile.file, + onProgress: (progress) => this.#updateProgress(item, progress), }); // Update progress - const progress = this.#progress.getValue(); - this.#progress.update({ completed: progress.completed + 1 }); - if (uploaded.status === TemporaryFileStatus.SUCCESS) { - this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.COMPLETE }); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.ERROR }); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } // Add to return value From 66cf11da2aabd05055ab70c5b2e418f17b5d9f1a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:43:26 +0100 Subject: [PATCH 28/43] feat: allows a `whitespace` option to notifications --- .../layouts/default/notification-layout-default.element.ts | 3 ++- .../src/packages/core/notification/notification.context.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts index bca5465b68b7..85a36bdec01a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts @@ -6,6 +6,7 @@ import { ifDefined, nothing, css, + styleMap, } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbNotificationDefaultData, UmbNotificationHandler } from '@umbraco-cms/backoffice/notification'; @@ -23,7 +24,7 @@ export class UmbNotificationLayoutDefaultElement extends LitElement { override render() { return html` -
${this.data.message}
+
${this.data.message}
${this.#renderStructuredList(this.data.structuredList)}
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts index 5bdbf98670a6..790da2268daa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts @@ -12,6 +12,7 @@ export interface UmbNotificationDefaultData { message: string; headline?: string; structuredList?: Record>; + whitespace?: 'normal' | 'pre-line' | 'pre-wrap' | 'nowrap' | 'pre'; } /** From bc58d72a8455efb5cd4f7c70e4d8686206705480 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:44:00 +0100 Subject: [PATCH 29/43] feat: validates uploads before trying to query the server --- .../src/assets/lang/en.ts | 1 + .../temporary-file-manager.class.ts | 56 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index dae39aa00f61..f36cfbf5b833 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -363,6 +363,7 @@ export default { disallowedFileType: 'Cannot upload this file, it does not have an approved file type', disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", invalidFileName: 'Cannot upload this file, it does not have a valid file name', + invalidFileSize: 'Cannot upload this file, it is too large', maxFileSize: 'Max file size is', mediaRoot: 'Media root', createFolderFailed: 'Failed to create a folder under parent id %0%', 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 eecb4e62b2d3..ce4395506a3e 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,17 +1,22 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; +import { UmbTemporaryFileConfigRepository } from './config/index.js'; import { TemporaryFileStatus, type UmbQueueHandlerCallback, type UmbTemporaryFileModel, type UmbUploadOptions, } from './types.js'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { observeMultiple, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; export class UmbTemporaryFileManager< UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, > extends UmbControllerBase { readonly #temporaryFileRepository = new UmbTemporaryFileRepository(this._host); + readonly #temporaryFileConfigRepository = new UmbTemporaryFileConfigRepository(this._host); + readonly #localization = new UmbLocalizationController(this._host); readonly #queue = new UmbArrayState([], (item) => item.temporaryUnique); public readonly queue = this.#queue.asObservable(); @@ -71,9 +76,58 @@ export class UmbTemporaryFileManager< return filesCompleted; } + async #validateItem(item: UploadableItem): Promise { + const maxFileSize = await this.observe(this.#temporaryFileConfigRepository.part('maxFileSize')).asPromise(); + if (maxFileSize && item.file.size > maxFileSize) { + const notification = await this.getContext(UMB_NOTIFICATION_CONTEXT); + notification.peek('warning', { + data: { + headline: 'Upload', + message: ` +${this.#localization.term('media_invalidFileSize')}: ${item.file.name} (${item.file.size} bytes) + +${this.#localization.term('media_maxFileSize')} ${maxFileSize} bytes + `, + whitespace: 'pre-line', + }, + }); + return false; + } + + const fileExtension = item.file.name.split('.').pop() ?? ''; + + const [allowedExtensions, disallowedExtensions] = await this.observe( + observeMultiple([ + this.#temporaryFileConfigRepository.part('allowedUploadedFileExtensions'), + this.#temporaryFileConfigRepository.part('disallowedUploadedFilesExtensions'), + ]), + ).asPromise(); + + if ( + (allowedExtensions && !allowedExtensions.includes(fileExtension)) || + (disallowedExtensions && disallowedExtensions.includes(fileExtension)) + ) { + const notification = await this.getContext(UMB_NOTIFICATION_CONTEXT); + notification.peek('warning', { + data: { + message: `${this.#localization.term('media_disallowedFileType')}: ${fileExtension}`, + }, + }); + return false; + } + + return true; + } + async #handleUpload(item: UploadableItem) { if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`); + const isValid = await this.#validateItem(item); + if (!isValid) { + this.#queue.updateOne(item.temporaryUnique, { ...item, status: TemporaryFileStatus.ERROR }); + return { ...item, status: TemporaryFileStatus.ERROR }; + } + const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file, (evt) => { // Update progress in percent if a callback is provided if (item.onProgress) item.onProgress((evt.loaded / evt.total) * 100); From 4ba5aa5cd267a497317ba4e6a6b004b708c39aa1 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:31:44 +0100 Subject: [PATCH 30/43] feat: adds `formatBytes` function to format numbers --- .../packages/core/utils/bytes/bytes.test.ts | 28 ++++++++++++ .../src/packages/core/utils/bytes/bytes.ts | 45 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts new file mode 100644 index 000000000000..5320f37ceea8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts @@ -0,0 +1,28 @@ +import { expect } from '@open-wc/testing'; +import { formatBytes } from './bytes.js'; + +describe('bytes', () => { + it('should format bytes as human-readable text', () => { + expect(formatBytes(0)).to.equal('0 Bytes'); + expect(formatBytes(1024)).to.equal('1 KB'); + expect(formatBytes(1024 * 1024)).to.equal('1 MB'); + expect(formatBytes(1024 * 1024 * 1024)).to.equal('1 GB'); + expect(formatBytes(1024 * 1024 * 1024 * 1024)).to.equal('1 TB'); + }); + + it('should format bytes as human-readable text with decimal places', () => { + expect(formatBytes(1024, { decimals: 0 })).to.equal('1 KB'); + expect(formatBytes(1587.2, { decimals: 2 })).to.equal('1.55 KB'); + }); + + it('should format bytes as human-readable text with different kilobytes', () => { + expect(formatBytes(1000, { kilo: 1000 })).to.equal('1 KB'); + expect(formatBytes(1000 * 1000, { kilo: 1000 })).to.equal('1 MB'); + expect(formatBytes(1000 * 1000 * 1000, { kilo: 1000 })).to.equal('1 GB'); + expect(formatBytes(1000 * 1000 * 1000 * 1000, { kilo: 1000 })).to.equal('1 TB'); + }); + + it('should format bytes as human-readable text with different culture', () => { + expect(formatBytes(1587.2, { decimals: 1, culture: 'da-DK' })).to.equal('1,6 KB'); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts new file mode 100644 index 000000000000..cd1f740fe9ee --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts @@ -0,0 +1,45 @@ +/* This Source Code has been derived from Lee Kelleher's Contentment. + * https://github.com/leekelleher/umbraco-contentment/blob/develop/src/Umbraco.Community.Contentment/DataEditors/Bytes/bytes.js + * Copyright © 2019 Lee Kelleher. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export interface IFormatBytesOptions { + /** + * Number of kilobytes, default is 1024. + * @example 1000 (1KB) or 1024 (1KiB) + */ + kilo?: number; + + /** + * Number of decimal places, default is 0. + * @example 0, 1, 2, 3, etc. + */ + decimals?: number; + + /** + * The culture to use for formatting the number itself, default is `undefined` which means the browser's default culture. + * @example 'en-GB', 'en-US', 'fr-FR', etc. + */ + culture?: string; +} + +/** + * Format bytes as human-readable text. + * @param {number} bytes - The number of bytes to format. + * @param {IFormatBytesOptions} opts - Optional settings. + * @returns {string} - The formatted bytes. + */ +export function formatBytes(bytes: number, opts?: IFormatBytesOptions): string { + if (bytes === 0) return '0 Bytes'; + + const k = opts?.kilo ?? 1024; + const dm = opts?.decimals ?? 0; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const n = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); + + return `${n.toLocaleString(opts?.culture)} ${sizes[i]}`; +} From 5fa5e092ab36aa6a10bbd040fd5bc92224d57233 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:31:53 +0100 Subject: [PATCH 31/43] chore: export all consts --- src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts index b13b2d868970..0c33d9bb70c2 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts @@ -212,7 +212,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/media', - consts: ["UMB_MEDIA_COLLECTION_ALIAS","UMB_MEDIA_COLLECTION_CONTEXT","UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS","UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS","UMB_MEDIA_TABLE_COLLECTION_VIEW_ALIAS","UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL","UMB_MEDIA_CREATE_OPTIONS_MODAL","UMB_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_MEDIA_REPOSITORY_ALIAS","UMB_BULK_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS","UMB_MEDIA_ENTITY_TYPE","UMB_MEDIA_ROOT_ENTITY_TYPE","UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE","UMB_MEDIA_MENU_ALIAS","UMB_IMAGE_CROPPER_EDITOR_MODAL","UMB_MEDIA_CAPTION_ALT_TEXT_MODAL","UMB_MEDIA_PICKER_MODAL","UMB_MEDIA_WORKSPACE_PATH","UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN","UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN","UMB_MEDIA_VARIANT_CONTEXT","UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_STORE_ALIAS","UMB_MEDIA_DETAIL_STORE_CONTEXT","UMB_MEDIA_ITEM_REPOSITORY_ALIAS","UMB_MEDIA_STORE_ALIAS","UMB_MEDIA_ITEM_STORE_CONTEXT","UMB_MEDIA_URL_REPOSITORY_ALIAS","UMB_MEDIA_URL_STORE_ALIAS","UMB_MEDIA_URL_STORE_CONTEXT","UMB_MEDIA_VALIDATION_REPOSITORY_ALIAS","UMB_MEDIA_TREE_REPOSITORY_ALIAS","UMB_MEDIA_TREE_STORE_ALIAS","UMB_MEDIA_TREE_ALIAS","UMB_MEDIA_TREE_PICKER_MODAL","UMB_MEDIA_TREE_STORE_CONTEXT","UMB_MEDIA_WORKSPACE_ALIAS","UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_MEDIA_WORKSPACE_CONTEXT"] + consts: ["UMB_MEDIA_COLLECTION_ALIAS","UMB_MEDIA_COLLECTION_CONTEXT","UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS","UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS","UMB_MEDIA_TABLE_COLLECTION_VIEW_ALIAS","UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL","UMB_MEDIA_CREATE_OPTIONS_MODAL","UMB_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_MEDIA_REPOSITORY_ALIAS","UMB_BULK_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS","UMB_MEDIA_ENTITY_TYPE","UMB_MEDIA_ROOT_ENTITY_TYPE","UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE","UMB_MEDIA_MENU_ALIAS","UMB_IMAGE_CROPPER_EDITOR_MODAL","UMB_MEDIA_CAPTION_ALT_TEXT_MODAL","UMB_MEDIA_PICKER_MODAL","UMB_MEDIA_WORKSPACE_PATH","UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN","UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN","UMB_MEDIA_VARIANT_CONTEXT","UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_STORE_ALIAS","UMB_MEDIA_DETAIL_STORE_CONTEXT","UMB_MEDIA_ITEM_REPOSITORY_ALIAS","UMB_MEDIA_STORE_ALIAS","UMB_MEDIA_ITEM_STORE_CONTEXT","UMB_MEDIA_URL_REPOSITORY_ALIAS","UMB_MEDIA_URL_STORE_ALIAS","UMB_MEDIA_URL_STORE_CONTEXT","UMB_MEDIA_VALIDATION_REPOSITORY_ALIAS","UMB_MEDIA_SEARCH_PROVIDER_ALIAS","UMB_MEDIA_TREE_REPOSITORY_ALIAS","UMB_MEDIA_TREE_STORE_ALIAS","UMB_MEDIA_TREE_ALIAS","UMB_MEDIA_TREE_PICKER_MODAL","UMB_MEDIA_TREE_STORE_CONTEXT","UMB_MEDIA_WORKSPACE_ALIAS","UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_MEDIA_WORKSPACE_CONTEXT"] }, { path: '@umbraco-cms/backoffice/member-group', @@ -364,7 +364,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/temporary-file', - consts: [] + consts: ["UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT","UMB_TEMPORARY_FILE_REPOSITORY_ALIAS","UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS"] }, { path: '@umbraco-cms/backoffice/themes', @@ -404,7 +404,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/user', - consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_USER_CONFIG_STORE_CONTEXT","UMB_CURRENT_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] + consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_CURRENT_USER_CONFIG_REPOSITORY_ALIAS","UMB_CURRENT_USER_CONFIG_STORE_ALIAS","UMB_CURRENT_USER_CONFIG_STORE_CONTEXT","UMB_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] }, { path: '@umbraco-cms/backoffice/utils', From a60dbc0deaab4cb18f98da378eb4be454c9450a4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:32:28 +0100 Subject: [PATCH 32/43] feat: exports bytes function --- .../packages/core/utils/bytes/{bytes.ts => bytes.function.ts} | 0 .../src/packages/core/utils/bytes/bytes.test.ts | 2 +- src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/{bytes.ts => bytes.function.ts} (100%) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts index 5320f37ceea8..1527f05aec2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts @@ -1,5 +1,5 @@ import { expect } from '@open-wc/testing'; -import { formatBytes } from './bytes.js'; +import { formatBytes } from './bytes.function.js'; describe('bytes', () => { it('should format bytes as human-readable text', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index 7e2cd4bcb7a4..e6aabbc8d175 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -1,3 +1,4 @@ +export * from './bytes/bytes.function.js'; export * from './debounce/debounce.function.js'; export * from './direction/index.js'; export * from './download/blob-download.function.js'; From b1b655d6a008feab182740d3e9f6d14239ee3074 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:35:27 +0100 Subject: [PATCH 33/43] feat: set decimals to default to 2, which works nicely with the Intl numberformat --- .../src/packages/core/utils/bytes/bytes.function.ts | 4 ++-- .../src/packages/core/utils/bytes/bytes.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts index cd1f740fe9ee..dce0deefcf7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts @@ -13,7 +13,7 @@ export interface IFormatBytesOptions { kilo?: number; /** - * Number of decimal places, default is 0. + * Number of decimal places, default is 2. * @example 0, 1, 2, 3, etc. */ decimals?: number; @@ -35,7 +35,7 @@ export function formatBytes(bytes: number, opts?: IFormatBytesOptions): string { if (bytes === 0) return '0 Bytes'; const k = opts?.kilo ?? 1024; - const dm = opts?.decimals ?? 0; + const dm = opts?.decimals ?? 2; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts index 1527f05aec2e..2177b722164e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts @@ -11,8 +11,8 @@ describe('bytes', () => { }); it('should format bytes as human-readable text with decimal places', () => { - expect(formatBytes(1024, { decimals: 0 })).to.equal('1 KB'); - expect(formatBytes(1587.2, { decimals: 2 })).to.equal('1.55 KB'); + expect(formatBytes(1587.2, { decimals: 0 })).to.equal('2 KB'); + expect(formatBytes(1587.2, { decimals: 1 })).to.equal('1.6 KB'); }); it('should format bytes as human-readable text with different kilobytes', () => { From d43127aeea8d8f64bee31e02c1c813849c44ab8c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:35:55 +0100 Subject: [PATCH 34/43] feat: use `formatBytes` to format the error message --- .../core/temporary-file/temporary-file-manager.class.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 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 ce4395506a3e..00494f5a1bbd 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 @@ -10,6 +10,7 @@ import { observeMultiple, UmbArrayState } from '@umbraco-cms/backoffice/observab import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { formatBytes } from '@umbraco-cms/backoffice/utils'; export class UmbTemporaryFileManager< UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, @@ -84,9 +85,9 @@ export class UmbTemporaryFileManager< data: { headline: 'Upload', message: ` -${this.#localization.term('media_invalidFileSize')}: ${item.file.name} (${item.file.size} bytes) +${this.#localization.term('media_invalidFileSize')}: ${item.file.name} (${formatBytes(item.file.size)}). -${this.#localization.term('media_maxFileSize')} ${maxFileSize} bytes +${this.#localization.term('media_maxFileSize')} ${formatBytes(maxFileSize)}. `, whitespace: 'pre-line', }, From 72a04905d48886fc310d459363c71cb13cac471d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:37:03 +0100 Subject: [PATCH 35/43] chore(mock): set max file size for mock to 1.4 GB --- .../mocks/handlers/temporary-file/temporary-file.handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts index 5f5e5474d102..ee72270fee22 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts @@ -25,7 +25,7 @@ export const handlers = [ ctx.json({ allowedUploadedFileExtensions: [], disallowedUploadedFilesExtensions: ['exe', 'dll', 'bat', 'msi'], - maxFileSize: 51200, + maxFileSize: 1468007, imageFileTypes: ['jpg', 'png', 'gif', 'jpeg', 'svg'], }), ); From 660334d05260d3e376b885ac5d888754864f645d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:41:05 +0100 Subject: [PATCH 36/43] feat: adds localization --- src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts | 5 +++-- src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index c6d6f63dbd20..56f2fa1b8127 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -345,6 +345,9 @@ export default { clickToUpload: 'Klik for at uploade', orClickHereToUpload: 'eller klik her for at vælge filer', disallowedFileType: 'Kan ikke uploade denne fil, den har ikke en godkendt filtype', + disallowedMediaType: "Kan ikke uploade denne fil, mediatypen med alias '%0%' er ikke tilladt her", + invalidFileName: 'Kan ikke uploade denne fil, den har et ugyldigt filnavn', + invalidFileSize: 'Kan ikke uploade denne fil, den er for stor', maxFileSize: 'Maks filstørrelse er', mediaRoot: 'Medie rod', moveToSameFolderFailed: 'Overordnet og destinations mappe kan ikke være den samme', @@ -353,8 +356,6 @@ export default { dragAndDropYourFilesIntoTheArea: 'Træk dine filer ind i dropzonen for, at uploade dem til\n mediebiblioteket.\n ', uploadNotAllowed: 'Upload er ikke tiladt på denne lokation', - disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", - invalidFileName: 'Cannot upload this file, it does not have a valid file name', }, member: { createNewMember: 'Opret et nyt medlem', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 995beb00d39c..3fbfe5fe3b06 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -376,6 +376,7 @@ export default { disallowedFileType: 'Cannot upload this file, it does not have an approved file type', disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", invalidFileName: 'Cannot upload this file, it does not have a valid file name', + invalidFileSize: 'Cannot upload this file, it is too large', maxFileSize: 'Max file size is', mediaRoot: 'Media root', createFolderFailed: 'Failed to create a folder under parent id %0%', From 60c344e41b2ae318827564a8dcbb39f742d393e2 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 08:56:27 +0100 Subject: [PATCH 37/43] Update src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts Co-authored-by: Lee Kelleher --- .../src/packages/core/utils/bytes/bytes.function.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts index dce0deefcf7d..47231c6c1037 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts @@ -1,9 +1,8 @@ /* This Source Code has been derived from Lee Kelleher's Contentment. * https://github.com/leekelleher/umbraco-contentment/blob/develop/src/Umbraco.Community.Contentment/DataEditors/Bytes/bytes.js + * SPDX-License-Identifier: MPL-2.0 * Copyright © 2019 Lee Kelleher. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + * Modifications are licensed under the MIT License. export interface IFormatBytesOptions { /** From 2c2206c42eabd7144fe18c07b1e9cbffd9c29629 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:00:28 +0100 Subject: [PATCH 38/43] chore: add end character to comment --- .../src/packages/core/utils/bytes/bytes.function.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts index 47231c6c1037..3664bcafebec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: MPL-2.0 * Copyright © 2019 Lee Kelleher. * Modifications are licensed under the MIT License. + */ export interface IFormatBytesOptions { /** From b6379fb96e1ba7636b17bd7bdce13bf66f4eb5d4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:01:20 +0100 Subject: [PATCH 39/43] feat: binds multiple text string to validation --- ...-editor-ui-multiple-text-string.element.ts | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts index 3a80825817bb..ca7420bfbbd8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts @@ -1,5 +1,5 @@ import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components'; @@ -7,6 +7,11 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { + UMB_SUBMITTABLE_WORKSPACE_CONTEXT, + UmbSubmittableWorkspaceContextBase, +} from '@umbraco-cms/backoffice/workspace'; /** * @element umb-property-editor-ui-multiple-text-string @@ -56,6 +61,21 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement @state() private _max = Infinity; + @query('#input', true) + protected _inputElement?: UmbInputMultipleTextStringElement; + + protected _validationContext = new UmbValidationContext(this); + + constructor() { + super(); + + this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (context) => { + if (context instanceof UmbSubmittableWorkspaceContextBase) { + context.addValidationContext(this._validationContext); + } + }); + } + #onChange(event: UmbChangeEvent) { event.stopPropagation(); const target = event.currentTarget as UmbInputMultipleTextStringElement; @@ -63,17 +83,31 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement this.dispatchEvent(new UmbPropertyValueChangeEvent()); } + // Prevent valid events from bubbling outside the message element + #onValid(event: Event) { + event.stopPropagation(); + } + + // Prevent invalid events from bubbling outside the message element + #onInvalid(event: Event) { + event.stopPropagation(); + } + override render() { return html` - - + + + + `; } } From fce920f0a8550274721ba5e627b0e29386564c38 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:01:31 +0100 Subject: [PATCH 40/43] chore: fixes event type --- .../input-multiple-text-string-item.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts index d2f07b510f36..aca689e71c18 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts @@ -65,12 +65,12 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U } // Prevent valid events from bubbling outside the message element - #onValid(event: any) { + #onValid(event: Event) { event.stopPropagation(); } // Prevent invalid events from bubbling outside the message element - #onInvalid(event: any) { + #onInvalid(event: Event) { event.stopPropagation(); } From bcca6ec2ac3e89632a5deb392068f600eaf9ed34 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:01:43 +0100 Subject: [PATCH 41/43] feat: adds new property editor ui for accepted file types --- .../property-editors/accepted-types/index.ts | 1 + .../accepted-types/manifests.ts | 15 ++ ...operty-editor-ui-accepted-types.element.ts | 132 ++++++++++++++++++ ...operty-editor-ui-accepted-types.stories.ts | 15 ++ .../property-editor-ui-accepted-types.test.ts | 21 +++ .../packages/property-editors/manifests.ts | 2 + 6 files changed, 186 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts new file mode 100644 index 000000000000..a3d8daffb16d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts @@ -0,0 +1 @@ +export * from './property-editor-ui-accepted-types.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts new file mode 100644 index 000000000000..f3b1a436e49c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts @@ -0,0 +1,15 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; + +export const manifest: ManifestPropertyEditorUi = { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.AcceptedTypes', + name: 'Accepted Types Property Editor UI', + element: () => import('./property-editor-ui-accepted-types.element.js'), + meta: { + label: 'Accepted Upload Types', + propertyEditorSchemaAlias: 'Umbraco.MultipleTextstring', + icon: 'icon-ordered-list', + group: 'lists', + supportsReadOnly: true, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts new file mode 100644 index 000000000000..e47bb258065f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts @@ -0,0 +1,132 @@ +import { UmbPropertyEditorUIMultipleTextStringElement } from '../multiple-text-string/property-editor-ui-multiple-text-string.element.js'; +import { css, customElement, html, nothing, state } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { + UmbTemporaryFileConfigRepository, + type UmbTemporaryFileConfigurationModel, +} from '@umbraco-cms/backoffice/temporary-file'; +import { formatBytes } from '@umbraco-cms/backoffice/utils'; + +/** + * @element umb-property-editor-ui-accepted-types + */ +@customElement('umb-property-editor-ui-accepted-types') +export class UmbPropertyEditorUIAcceptedTypesElement + extends UmbPropertyEditorUIMultipleTextStringElement + implements UmbPropertyEditorUiElement +{ + @state() + protected _acceptedTypes: string[] = []; + + @state() + protected _disallowedTypes: string[] = []; + + @state() + protected _maxFileSize?: number | null; + + #temporaryFileConfigRepository = new UmbTemporaryFileConfigRepository(this); + + override async connectedCallback() { + super.connectedCallback(); + + await this.#temporaryFileConfigRepository.initialized; + this.observe(this.#temporaryFileConfigRepository.all(), (config) => { + if (!config) return; + + this.#addValidators(config); + + this._acceptedTypes = config.allowedUploadedFileExtensions; + this._disallowedTypes = config.disallowedUploadedFilesExtensions; + this._maxFileSize = config.maxFileSize ? config.maxFileSize * 1024 : null; + }); + } + + #addValidators(config: UmbTemporaryFileConfigurationModel) { + this._inputElement?.addValidator( + 'badInput', + () => { + let message = 'One of the extensions is not valid.'; + if (config.allowedUploadedFileExtensions.length) { + message += ` The extension must be one of the following: ${config.allowedUploadedFileExtensions.join(', ')}.`; + } + if (config.disallowedUploadedFilesExtensions.length) { + message += ` The extension must not be one of the following: ${config.disallowedUploadedFilesExtensions.join(', ')}.`; + } + return message; + }, + () => { + const extensions = this._inputElement?.items; + if (!extensions) return false; + if ( + config.allowedUploadedFileExtensions.length && + !config.allowedUploadedFileExtensions.some((ext) => extensions.includes(ext)) + ) { + return true; + } + if (config.disallowedUploadedFilesExtensions.some((ext) => extensions.includes(ext))) { + return true; + } + return false; + }, + ); + } + + #renderAcceptedTypes() { + if (!this._acceptedTypes.length && !this._disallowedTypes.length && !this._maxFileSize) { + return nothing; + } + return html` + +

+ Regardless of the accepted extensions below, the following limitations apply system-wide due to the server + configuration: +

+ ${this._acceptedTypes.length + ? html`

+ You can only upload files of the following types: ${this._acceptedTypes.join(', ')}. +

` + : nothing} + ${this._disallowedTypes.length + ? html`

+ Files of the following types are not allowed: ${this._disallowedTypes.join(', ')}. +

` + : nothing} + ${this._maxFileSize + ? html` +

+ The maximum file size is + + ${formatBytes(this._maxFileSize, { decimals: 2 })} . +

+ ` + : nothing} +
+ `; + } + + override render() { + return html` ${this.#renderAcceptedTypes()} ${super.render()} `; + } + + static override readonly styles = [ + UmbTextStyles, + css` + #notice { + --uui-color-divider-standalone: var(--uui-color-warning-standalone); + border: 1px solid var(--uui-color-divider-standalone); + background-color: var(--uui-color-warning); + color: var(--uui-color-warning-contrast); + margin-bottom: var(--uui-size-layout-1); + `, + ]; +} + +export default UmbPropertyEditorUIAcceptedTypesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-accepted-types': UmbPropertyEditorUIAcceptedTypesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.stories.ts new file mode 100644 index 000000000000..324613d3d64a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.stories.ts @@ -0,0 +1,15 @@ +import type { UmbPropertyEditorUIAcceptedTypesElement } from './property-editor-ui-accepted-types.element.js'; +import type { Meta, StoryFn } from '@storybook/web-components'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './property-editor-ui-accepted-types.element.js'; + +export default { + title: 'Property Editor UIs/Accepted Types', + component: 'umb-property-editor-ui-accepted-types', + id: 'umb-property-editor-ui-accepted-types', +} as Meta; + +export const AAAOverview: StoryFn = () => + html``; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.test.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.test.ts new file mode 100644 index 000000000000..bcd57e410246 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.test.ts @@ -0,0 +1,21 @@ +import { UmbPropertyEditorUIAcceptedTypesElement } from './property-editor-ui-accepted-types.element.js'; +import { expect, fixture, html } from '@open-wc/testing'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUIUploadFieldElement', () => { + let element: UmbPropertyEditorUIAcceptedTypesElement; + + beforeEach(async () => { + element = await fixture(html` `); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUIAcceptedTypesElement); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts index 55595ce29197..4ded362fc797 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts @@ -1,3 +1,4 @@ +import { manifest as acceptedType } from './accepted-types/manifests.js'; import { manifest as colorEditor } from './color-swatches-editor/manifests.js'; import { manifest as numberRange } from './number-range/manifests.js'; import { manifest as orderDirection } from './order-direction/manifests.js'; @@ -38,6 +39,7 @@ export const manifests: Array = [ ...textBoxManifests, ...toggleManifests, ...contentPickerManifests, + acceptedType, colorEditor, numberRange, orderDirection, From 9299cb30cefd533b01f6021d093905bdfc546cc3 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:02:00 +0100 Subject: [PATCH 42/43] feat: changes the upload field to use the property editor ui for accepted file types --- .../media/property-editors/upload-field/Umbraco.UploadField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts index fe697d898960..3fcf5f86d273 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts @@ -11,7 +11,7 @@ export const manifest: ManifestPropertyEditorSchema = { { alias: 'fileExtensions', label: 'Accepted file extensions', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.AcceptedTypes', }, ], }, From efd9f3fd540893297cf0ad205ef7a6b9f3a49c00 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:09:35 +0100 Subject: [PATCH 43/43] adds localization --- .../src/assets/lang/en.ts | 6 ++++++ ...operty-editor-ui-accepted-types.element.ts | 20 +++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index f36cfbf5b833..f7ecbb896a62 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -372,6 +372,8 @@ export default { fileSecurityValidationFailure: 'One or more file security validations have failed', moveToSameFolderFailed: 'Parent and destination folders cannot be the same', uploadNotAllowed: 'Upload is not allowed in this location.', + noticeExtensionsServerOverride: + 'Regardless of the allowed file types, the following limitations apply system-wide due to the server configuration:', }, member: { '2fa': 'Two-Factor Authentication', @@ -885,6 +887,7 @@ export default { retrieve: 'Retrieve', retry: 'Retry', rights: 'Permissions', + serverConfiguration: 'Server Configuration', scheduledPublishing: 'Scheduled Publishing', umbracoInfo: 'Umbraco info', search: 'Search', @@ -2134,6 +2137,9 @@ export default { numberMinimum: "Value must be greater than or equal to '%0%'.", numberMaximum: "Value must be less than or equal to '%0%'.", numberMisconfigured: "Minimum value '%0%' must be less than the maximum value '%1%'.", + invalidExtensions: 'One or more of the extensions are invalid.', + allowedExtensions: 'Allowed extensions are:', + disallowedExtensions: 'Disallowed extensions are:', }, healthcheck: { checkSuccessMessage: "Value is set to the recommended value: '%0%'.", diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts index e47bb258065f..ec1a18fa6268 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-types.element.ts @@ -46,12 +46,12 @@ export class UmbPropertyEditorUIAcceptedTypesElement this._inputElement?.addValidator( 'badInput', () => { - let message = 'One of the extensions is not valid.'; + let message = this.localize.term('validation_invalidExtensions'); if (config.allowedUploadedFileExtensions.length) { - message += ` The extension must be one of the following: ${config.allowedUploadedFileExtensions.join(', ')}.`; + message += ` ${this.localize.term('validation_allowedExtensions')} ${config.allowedUploadedFileExtensions.join(', ')}`; } if (config.disallowedUploadedFilesExtensions.length) { - message += ` The extension must not be one of the following: ${config.disallowedUploadedFilesExtensions.join(', ')}.`; + message += ` ${this.localize.term('validation_disallowedExtensions')} ${config.disallowedUploadedFilesExtensions.join(', ')}`; } return message; }, @@ -77,25 +77,23 @@ export class UmbPropertyEditorUIAcceptedTypesElement return nothing; } return html` - -

- Regardless of the accepted extensions below, the following limitations apply system-wide due to the server - configuration: -

+ +

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

${this._acceptedTypes.length ? html`

- You can only upload files of the following types: ${this._acceptedTypes.join(', ')}. + ${this.localize.term('validation_allowedExtensions')} ${this._acceptedTypes.join(', ')}

` : nothing} ${this._disallowedTypes.length ? html`

- Files of the following types are not allowed: ${this._disallowedTypes.join(', ')}. + ${this.localize.term('validation_disallowedExtensions')} + ${this._disallowedTypes.join(', ')}

` : nothing} ${this._maxFileSize ? html`

- The maximum file size is + ${this.localize.term('media_maxFileSize')} ${formatBytes(this._maxFileSize, { decimals: 2 })} .