V15: Improve the dropzone for Upload Field (#18840)

* feat: maps up the CANCELLED status

* feat: uses the new dropzone input to render the dropzone

* feat: adds support for differing server urls

* chore: avoids a breaking change by storing the temporary file
This commit is contained in:
Jacob Overgaard
2025-03-27 11:22:03 +01:00
committed by GitHub
parent a71ebe1902
commit 164868f32a
2 changed files with 82 additions and 193 deletions

View File

@@ -70,7 +70,6 @@ export class UmbDropzoneManager extends UmbControllerBase {
}
/**
* @param isAllowed
* @deprecated Not used anymore; this method will be removed in Umbraco 17.
*/
public setIsFoldersAllowed(isAllowed: boolean) {
@@ -128,7 +127,9 @@ export class UmbDropzoneManager extends UmbControllerBase {
const uploaded = await this.#tempFileManager.uploadOne(item.temporaryFile);
// Update progress
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
if (uploaded.status === TemporaryFileStatus.CANCELLED) {
this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED);
} else if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE);
} else {
this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR);
@@ -226,7 +227,8 @@ export class UmbDropzoneManager extends UmbControllerBase {
async #handleFile(item: UmbUploadableFile, mediaTypeUnique: string) {
// Upload the file as a temporary file and update progress.
const temporaryFile = await this.#uploadAsTemporaryFile(item);
const temporaryFile = await this.#tempFileManager.uploadOne(item.temporaryFile);
if (temporaryFile.status === TemporaryFileStatus.CANCELLED) {
this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED);
return;
@@ -257,10 +259,6 @@ export class UmbDropzoneManager extends UmbControllerBase {
}
}
#uploadAsTemporaryFile(item: UmbUploadableFile) {
return this.#tempFileManager.uploadOne(item.temporaryFile);
}
// Media types
async #getMediaTypeOptions(item: UmbUploadableItem): Promise<Array<UmbAllowedMediaTypeModel>> {
// Check the parent which children media types are allowed

View File

@@ -1,33 +1,28 @@
import type { MediaValueType } from '../../property-editors/upload-field/types.js';
import type { ManifestFileUploadPreview } from './file-upload-preview.extension.js';
import { getMimeTypeFromExtension } from './utils.js';
import {
css,
html,
nothing,
ifDefined,
customElement,
property,
query,
state,
when,
} from '@umbraco-cms/backoffice/external/lit';
import { formatBytes, stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { css, customElement, html, ifDefined, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbFileDropzoneItemStatus, UmbInputDropzoneDashedStyles } from '@umbraco-cms/backoffice/dropzone';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTemporaryFileManager, TemporaryFileStatus } from '@umbraco-cms/backoffice/temporary-file';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
import type {
UmbDropzoneChangeEvent,
UmbInputDropzoneElement,
UmbUploadableFile,
} from '@umbraco-cms/backoffice/dropzone';
import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-input-upload-field')
export class UmbInputUploadFieldElement extends UmbLitElement {
@property({ type: Object })
@property({ type: Object, attribute: false })
set value(value: MediaValueType) {
this.#src = value?.src ?? '';
this.#setPreviewAlias();
}
get value(): MediaValueType {
return {
@@ -42,39 +37,43 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
* @type {Array<string>}
* @default
*/
@property({ type: Array })
set allowedFileExtensions(value: Array<string>) {
this.#setExtensions(value);
}
get allowedFileExtensions(): Array<string> | undefined {
return this._extensions;
}
@property({
type: Array,
attribute: 'allowed-file-extensions',
converter(value) {
if (typeof value === 'string') {
return value.split(',').map((ext) => ext.trim());
}
return value;
},
})
allowedFileExtensions?: Array<string>;
@state()
public temporaryFile?: UmbTemporaryFileModel;
@state()
private _progress = 0;
@state()
private _extensions?: string[];
@state()
private _previewAlias?: string;
@query('#dropzone')
private _dropzone?: UUIFileDropzoneElement;
#manager = new UmbTemporaryFileManager(this);
@state()
private _serverUrl = '';
#manifests: Array<ManifestFileUploadPreview> = [];
override updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
super.updated(changedProperties);
constructor() {
super();
if (changedProperties.has('value') && changedProperties.get('value')?.src !== this.value.src) {
this.#setPreviewAlias();
}
this.consumeContext(UMB_APP_CONTEXT, (context) => {
this._serverUrl = context.getServerUrl();
});
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#clearObjectUrl();
}
async #getManifests() {
@@ -87,15 +86,6 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
return this.#manifests;
}
#setExtensions(extensions: Array<string>) {
if (!extensions?.length) {
this._extensions = undefined;
return;
}
// TODO: The dropzone uui component does not support file extensions without a dot. Remove this when it does.
this._extensions = extensions?.map((extension) => `.${extension}`);
}
async #setPreviewAlias(): Promise<void> {
this._previewAlias = await this.#getPreviewElementAlias();
}
@@ -151,47 +141,22 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
return getMimeTypeFromExtension('.' + extension);
}
async #onUpload(e: UUIFileDropzoneEvent) {
try {
//Property Editor for Upload field will always only have one file.
this.temporaryFile = {
temporaryUnique: UmbId.new(),
status: TemporaryFileStatus.WAITING,
file: e.detail.files[0],
onProgress: (p) => {
this._progress = Math.ceil(p);
},
abortController: new AbortController(),
};
const uploaded = await this.#manager.uploadOne(this.temporaryFile);
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.temporaryFile.status = TemporaryFileStatus.SUCCESS;
const blobUrl = URL.createObjectURL(this.temporaryFile.file);
this.value = { src: blobUrl };
this.dispatchEvent(new UmbChangeEvent());
} else {
this.temporaryFile.status = TemporaryFileStatus.ERROR;
this.requestUpdate('temporaryFile');
}
} catch {
// If we still have a temporary file, set it to error.
if (this.temporaryFile) {
this.temporaryFile.status = TemporaryFileStatus.ERROR;
this.requestUpdate('temporaryFile');
}
// If the error was caused by the upload being aborted, do not show an error message.
}
}
#handleBrowse(e: Event) {
if (!this._dropzone) return;
async #onUpload(e: UmbDropzoneChangeEvent) {
e.stopImmediatePropagation();
this._dropzone.browse();
const target = e.target as UmbInputDropzoneElement;
const file = target.value?.[0];
if (file?.status !== UmbFileDropzoneItemStatus.COMPLETE) return;
this.temporaryFile = (file as UmbUploadableFile).temporaryFile;
this.#clearObjectUrl();
const blobUrl = URL.createObjectURL(this.temporaryFile.file);
this.value = { src: blobUrl };
this.dispatchEvent(new UmbChangeEvent());
}
override render() {
@@ -199,69 +164,28 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
return this.#renderDropzone();
}
return html`
${this.temporaryFile ? this.#renderUploader() : nothing}
${this.value.src && this._previewAlias ? this.#renderFile(this.value.src) : nothing}
`;
if (this.value?.src && this._previewAlias) {
return this.#renderFile(this.value.src);
}
return nothing;
}
#renderDropzone() {
return html`
<uui-file-dropzone
<umb-input-dropzone
id="dropzone"
label="dropzone"
disallowFolderUpload
accept=${ifDefined(this._extensions?.join(', '))}
@change=${this.#onUpload}
@click=${this.#handleBrowse}>
<uui-button label=${this.localize.term('media_clickToUpload')} @click=${this.#handleBrowse}></uui-button>
</uui-file-dropzone>
`;
}
#renderUploader() {
if (!this.temporaryFile) return nothing;
return html`
<div id="temporaryFile">
<div id="fileIcon">
${when(
this.temporaryFile.status === TemporaryFileStatus.SUCCESS,
() => html`<umb-icon name="check" color="green"></umb-icon>`,
)}
${when(
this.temporaryFile.status === TemporaryFileStatus.ERROR,
() => html`<umb-icon name="wrong" color="red"></umb-icon>`,
)}
</div>
<div id="fileDetails">
<div id="fileName">${this.temporaryFile.file.name}</div>
<div id="fileSize">${formatBytes(this.temporaryFile.file.size, { decimals: 2 })}: ${this._progress}%</div>
${when(
this.temporaryFile.status === TemporaryFileStatus.WAITING,
() => html`<div id="progress"><uui-loader-bar progress=${this._progress}></uui-loader-bar></div>`,
)}
${when(
this.temporaryFile.status === TemporaryFileStatus.ERROR,
() => html`<div id="error">An error occured</div>`,
)}
</div>
<div id="fileActions">
${when(
this.temporaryFile.status === TemporaryFileStatus.WAITING,
() => html`
<uui-button compact @click=${this.#handleRemove} label=${this.localize.term('general_cancel')}>
<uui-icon name="remove"></uui-icon>${this.localize.term('general_cancel')}
</uui-button>
`,
() => this.#renderButtonRemove(),
)}
</div>
</div>
disable-folder-upload
accept=${ifDefined(this._extensions?.join(','))}
@change=${this.#onUpload}></umb-input-dropzone>
`;
}
#renderFile(src: string) {
if (!src.startsWith('blob:')) {
src = this._serverUrl + src;
}
return html`
<div id="wrapper">
<div id="wrapperInner">
@@ -288,13 +212,25 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
// If the upload promise happens to be in progress, cancel it.
this.temporaryFile?.abortController?.abort();
this.#clearObjectUrl();
this.value = { src: undefined };
this.temporaryFile = undefined;
this._progress = 0;
this.dispatchEvent(new UmbChangeEvent());
}
/**
* If there is a former File, revoke the object URL.
*/
#clearObjectUrl(): void {
if (this.value?.src?.startsWith('blob:')) {
URL.revokeObjectURL(this.value.src);
}
}
static override readonly styles = [
UmbTextStyles,
UmbInputDropzoneDashedStyles,
css`
:host {
position: relative;
@@ -323,51 +259,6 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
width: fit-content;
max-width: 100%;
}
#temporaryFile {
display: grid;
grid-template-columns: auto auto auto;
width: fit-content;
max-width: 100%;
margin: var(--uui-size-layout-1) 0;
padding: var(--uui-size-space-3);
border: 1px dashed var(--uui-color-divider-emphasis);
}
#fileIcon,
#fileActions {
place-self: center center;
padding: 0 var(--uui-size-layout-1);
}
#fileName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--uui-size-5);
}
#fileSize {
font-size: var(--uui-font-size-small);
color: var(--uui-color-text-alt);
}
#error {
color: var(--uui-color-danger);
}
uui-file-dropzone {
position: relative;
display: block;
padding: 3px; /** Dropzone background is blurry and covers slightly into other elements. Hack to avoid this */
}
uui-file-dropzone::after {
content: '';
position: absolute;
inset: 0;
cursor: pointer;
border: 1px dashed var(--uui-color-divider-emphasis);
}
`,
];
}