From 9e46e7c68568755d681cb4697d3cd1183a659eb5 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:26:56 +0100 Subject: [PATCH] tempfilemanager --- .../input-upload-field-file.element.ts | 32 +++++++- .../input-upload-field.element.ts | 70 +++++++++++++----- ...property-editor-ui-upload-field.element.ts | 5 +- .../temporary-file-badge.element.ts | 2 + .../temporary-file-manager.class.ts | 74 +++++++++++++++++++ 5 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts index 622058876a..1a41cf38c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts @@ -1,4 +1,5 @@ -import { css, html, nothing, until, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; +import { html, until, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; type FileItem = { @@ -8,12 +9,15 @@ type FileItem = { @customElement('umb-input-upload-field-file') export class UmbInputUploadFieldFileElement extends UmbLitElement { + @property({ type: String }) + path = ''; + /** * @description The file to be rendered. * @type {File} * @required */ - @property({ type: File, attribute: false }) + @property({ attribute: false }) set file(value: File) { this.#fileItem = new Promise((resolve) => { /** @@ -40,15 +44,35 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement { } #fileItem!: Promise; + #serverUrl = ''; - render = () => until(this.#renderFileItem(), html``); + constructor() { + super(); + this.consumeContext(UMB_APP_CONTEXT, (instance) => { + this.#serverUrl = instance.getServerUrl(); + }); + } + + // TODO Better way to do this.... + render = () => { + if (this.path) { + return html``; + } else { + return until(this.#renderFileItem(), html``); + } + }; + + // render = () => until(this.#renderFileItem(), html``); async #renderFileItem() { const fileItem = await this.#fileItem; return html``; + alt=${fileItem.name}> `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index ec6f0d9c78..0b4a6506ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -1,13 +1,15 @@ +import { UmbId } from '../../index.js'; +import { TemporaryFileQueueItem, UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js'; import { css, html, nothing, - map, ifDefined, customElement, property, query, state, + repeat, } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; @@ -50,7 +52,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) multiple = false; @state() - _currentFiles: File[] = []; + _currentFiles: Array = []; @state() extensions?: string[]; @@ -58,10 +60,20 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) @query('#dropzone') private _dropzone?: UUIFileDropzoneElement; + #manager; + protected getFormElement() { return undefined; } + constructor() { + super(); + this.#manager = new UmbTemporaryFileManager(this); + + this.observe(this.#manager.isReady, (value) => (this.error = !value)); + this.observe(this.#manager.items, (value) => (this._currentFiles = value)); + } + connectedCallback(): void { super.connectedCallback(); this.#setExtensions(); @@ -85,12 +97,19 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) } #setFiles(files: File[]) { - this._currentFiles = [...this._currentFiles, ...files]; + const items = files.map( + (file): TemporaryFileQueueItem => ({ + id: UmbId.new(), + file, + status: 'waiting', + }), + ); + this.#manager.upload(items); - //TODO: set keys when possible, not names - this.keys = this._currentFiles.map((file) => file.name); - this.dispatchEvent(new CustomEvent('change', { bubbles: true })); + this.keys = items.map((item) => item.id); this.value = this.keys.join(','); + + this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } #handleBrowse() { @@ -99,9 +118,11 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) } render() { - return html`${this.#renderFiles()} ${this.#renderDropzone()}`; + return html`${this.#renderUploadedFiles()} ${this.#renderDropzone()}${this.#renderButtonRemove()}`; } + //TODO When the property editor gets saved, it seems that the property editor gets the file path from the server rather than key/id. + // Image/files needs to be displayed from a previous save (not just when it just got uploaded). #renderDropzone() { if (!this.multiple && this._currentFiles.length) return nothing; return html` @@ -115,22 +136,33 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) `; } - - #renderFiles() { + #renderUploadedFiles() { if (!this._currentFiles.length) return nothing; - return html`
- ${map(this._currentFiles, (file) => { - return html``; - })} -
- - Remove file(s) - `; + return html`
+ ${repeat( + this._currentFiles, + (item) => item.id + item.status, + (item) => + html`
+ + ${item.status === 'waiting' ? html`` : nothing} +
`, + )} +
`; + } + + #renderButtonRemove() { + if (!this._currentFiles.length) return; + return html` + Remove file(s) + `; } #handleRemove() { - // Remove via endpoint? - this._currentFiles = []; + const ids = this._currentFiles.map((item) => item.id) as string[]; + this.#manager.remove(ids); + + this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/upload-field/property-editor-ui-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/upload-field/property-editor-ui-upload-field.element.ts index 38d4cf2510..e1ceabc519 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/upload-field/property-editor-ui-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/upload-field/property-editor-ui-upload-field.element.ts @@ -1,6 +1,6 @@ import { UmbInputUploadFieldElement } from '../../../components/input-upload-field/input-upload-field.element.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; @@ -34,7 +34,8 @@ export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement impleme return html``; + .fileExtensions="${this._fileExtensions}" + .value=${this.value}>`; } static styles = [UmbTextStyles]; 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 c64d25b7cd..a507830b8a 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 @@ -69,3 +69,5 @@ declare global { 'umb-temporary-file-badge': UmbTemporaryFileBadgeElement; } } + +export default UmbTemporaryFileBadgeElement; 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 new file mode 100644 index 0000000000..5e39ef14f7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -0,0 +1,74 @@ +import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; +import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export type TemporaryFileStatus = 'complete' | 'waiting' | 'error'; + +export interface TemporaryFileQueueItem { + id: string; + file: File; + status?: TemporaryFileStatus; + //type?: string; +} + +export class UmbTemporaryFileManager { + #temporaryFileRepository; + + #items = new UmbArrayState([], (item) => item.id); + public readonly items = this.#items.asObservable(); + + #isReady = new UmbBooleanState(true); + public readonly isReady = this.#isReady.asObservable(); + + constructor(host: UmbControllerHostElement) { + this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); + //this.items.subscribe(() => this.handleQueue()); + } + + uploadOne(id: string, file: File, status: TemporaryFileStatus = 'waiting') { + this.#items.appendOne({ id, file, status }); + this.handleQueue(); + } + + upload(items: Array) { + this.#items.append(items); + this.handleQueue(); + } + + removeOne(id: string) { + this.#items.removeOne(id); + } + + remove(ids: Array) { + this.#items.remove(ids); + } + + private async handleQueue() { + const items = this.#items.getValue(); + + if (!items.length && this.getIsReady()) return; + + this.#isReady.next(false); + + items.forEach(async (item) => { + if (item.status !== 'waiting') return; + + const { error } = await this.#temporaryFileRepository.upload(item.id, item.file); + await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown + + if (error) { + this.#items.updateOne(item.id, { ...item, status: 'error' }); + } else { + this.#items.updateOne(item.id, { ...item, status: 'complete' }); + } + }); + + if (!items.find((item) => item.status === 'waiting') && !this.getIsReady()) { + this.#isReady.next(true); + } + } + + getIsReady() { + return this.#items.getValue(); + } +}