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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user