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..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 @@ -722,7 +722,70 @@ export const data: Array = [ values: [ { alias: 'fileExtensions', - value: ['jpg', 'jpeg', 'png', 'pdf'], + 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', @@ -912,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' }, 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, 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); 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..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 @@ -16,26 +16,38 @@ export const data: Array = [ isTrashed: false, mediaType: { id: 'media-type-1-id', - icon: 'icon-bug', + icon: 'icon-picture', }, values: [ + { + editorAlias: 'Umbraco.UploadField', + alias: 'mediaPicker', + value: { + src: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + }, { 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', updateDate: '2023-02-06T15:31:51.354764', }, ], - urls: [], + urls: [ + { + culture: null, + url: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + ], }, { hasChildren: false, @@ -51,14 +63,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 +95,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 +120,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 +143,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 +173,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 +203,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 +233,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', 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/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, ]; 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)); + }), ]; 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), + ); }), ]; 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/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%; 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..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 @@ -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 }); } @@ -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/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', 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/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index ff7cf3939db9..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 @@ -48,6 +48,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 31a89e174465..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 @@ -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 } 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,15 +32,17 @@ 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. if (item.folder?.name) return; this.#collectionContext?.updatePlaceholderStatus(item.unique, item.status); + this.#collectionContext?.updatePlaceholderProgress(item.unique, item.progress); }); }, '_observeProgressItems', @@ -57,7 +56,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 +87,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { ${when(this._progress >= 0, () => html``)} ; url?: string; status?: UmbFileDropzoneItemStatus; + /** + * 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 77fb522a72ea..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 @@ -113,9 +113,12 @@ 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` - + `; } @@ -133,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; 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..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 @@ -19,6 +19,8 @@ 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'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; /** * Manages the dropzone and uploads folders and files to the server. @@ -48,9 +50,16 @@ export class UmbDropzoneManager extends UmbControllerBase { readonly #progressItems = new UmbArrayState([], (x) => x.unique); public readonly progressItems = this.#progressItems.asObservable(); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #localization = new UmbLocalizationController(this); + constructor(host: UmbControllerHost) { super(host); this.#host = host; + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); } public setIsFoldersAllowed(isAllowed: boolean) { @@ -105,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 @@ -134,13 +141,18 @@ export class UmbDropzoneManager extends UmbControllerBase { async #createOneMediaItem(item: UmbUploadableItem) { const options = await this.#getMediaTypeOptions(item); if (!options.length) { - return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + this.#notificationContext?.peek('warning', { + data: { + message: `${this.#localization.term('media_disallowedFileType')}: ${item.temporaryFile?.file.name}.`, + }, + }); + 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) { @@ -154,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; } @@ -177,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; } @@ -186,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); } } @@ -196,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); } } @@ -206,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), }); } @@ -292,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, @@ -309,6 +326,7 @@ export class UmbDropzoneManager extends UmbControllerBase { unique: UmbId.new(), parentUnique, status: UmbFileDropzoneItemStatus.WAITING, + progress: 0, temporaryFile: { file, temporaryUnique: UmbId.new() }, }); } @@ -319,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/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 { 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; }