Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export function isCancelError(error: unknown): error is CancelError {
* @param promise
*/
export function isCancelablePromise<T>(promise: unknown): promise is CancelablePromise<T> {
return (promise as CancelablePromise<T>).cancel !== undefined;
return (promise as CancelablePromise<T>)?.cancel !== undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ export class UmbResourceController extends UmbControllerBase {
});
});

if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () => {
promise.cancel();
});
}

return promise;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface XhrRequestOptions {
headers?: Record<string, string>;
responseHeader?: string;
onProgress?: (event: ProgressEvent) => void;
abortSignal?: AbortSignal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,22 @@ export class UmbTemporaryFileManager<

const isValid = await this.#validateItem(item);
if (!isValid) {
this.#queue.updateOne(item.temporaryUnique, { ...item, status: TemporaryFileStatus.ERROR });
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);
});
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);
},
item.abortSignal,
);
const status = error ? TemporaryFileStatus.ERROR : TemporaryFileStatus.SUCCESS;

this.#queue.updateOne(item.temporaryUnique, { ...item, status });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export class UmbTemporaryFileRepository extends UmbRepositoryBase {
* @returns {*}
* @memberof UmbTemporaryFileRepository
*/
upload(id: string, file: File, onProgress?: (progress: ProgressEvent) => void) {
return this.#source.create(id, file, onProgress);
upload(id: string, file: File, onProgress?: (progress: ProgressEvent) => void, abortSignal?: AbortSignal) {
return this.#source.create(id, file, onProgress, abortSignal);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class UmbTemporaryFileServerDataSource {
id: string,
file: File,
onProgress?: (progress: ProgressEvent) => void,
abortSignal?: AbortSignal,
): Promise<UmbDataSourceResponse<PostTemporaryFileResponse>> {
const body = new FormData();
body.append('Id', id);
Expand All @@ -41,6 +42,7 @@ export class UmbTemporaryFileServerDataSource {
responseHeader: 'Umb-Generated-Resource',
body,
onProgress,
abortSignal,
});
return tryExecuteAndNotify(this.#host, xhrRequest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface UmbTemporaryFileModel {
temporaryUnique: string;
status?: TemporaryFileStatus;
onProgress?: (progress: number) => void;
abortSignal?: AbortSignal;
}

export type UmbQueueHandlerCallback<TItem extends UmbTemporaryFileModel> = (item: TItem) => Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { MediaValueType } from '../../property-editors/upload-field/types.js';
import { getMimeTypeFromExtension } from './utils.js';
import type { ManifestFileUploadPreview } from './file-upload-preview.extension.js';
import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { getMimeTypeFromExtension } from './utils.js';
import {
css,
html,
Expand All @@ -13,15 +10,18 @@
property,
query,
state,
type PropertyValueMap,
when,
} from '@umbraco-cms/backoffice/external/lit';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { formatBytes, stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';

import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { UmbId } from '@umbraco-cms/backoffice/id';
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 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 {
Expand All @@ -35,7 +35,6 @@
temporaryFileId: this.temporaryFile?.temporaryUnique,
};
}

#src = '';

/**
Expand All @@ -54,6 +53,9 @@
@state()
public temporaryFile?: UmbTemporaryFileModel;

@state()
private _progress = 0;

@state()
private _extensions?: string[];

Expand All @@ -67,12 +69,11 @@

#manifests: Array<ManifestFileUploadPreview> = [];

constructor() {
super();
}
#uploadAbort?: AbortController;

override updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
super.updated(changedProperties);

if (changedProperties.has('value') && changedProperties.get('value')?.src !== this.value.src) {
this.#setPreviewAlias();
}
Expand Down Expand Up @@ -108,7 +109,13 @@
stringOrStringArrayContains(manifest.forMimeTypes, '*/*'),
)?.alias;

const mimeType = this.#getMimeTypeFromPath(this.value.src);
let mimeType: string | null = null;
if (this.temporaryFile?.file) {
mimeType = this.temporaryFile.file.type;
} else {
mimeType = this.#getMimeTypeFromPath(this.value.src);
}

Check notice on line 118 in src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

ℹ Getting worse: Complex Method

UmbInputUploadFieldElement.getPreviewElementAlias increases in cyclomatic complexity from 9 to 11, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
if (!mimeType) return fallbackAlias;

// Check for an exact match
Expand Down Expand Up @@ -148,23 +155,43 @@

async #onUpload(e: UUIFileDropzoneEvent) {
//Property Editor for Upload field will always only have one file.
const item: UmbTemporaryFileModel = {
this.temporaryFile = {
temporaryUnique: UmbId.new(),
status: TemporaryFileStatus.WAITING,
file: e.detail.files[0],
};

const upload = this.#manager.uploadOne(item);
try {
this.#uploadAbort = new AbortController();
const uploaded = await this.#manager.uploadOne({
...this.temporaryFile,
onProgress: (p) => {
this._progress = Math.ceil(p);
},
abortSignal: this.#uploadAbort.signal,
});

if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.temporaryFile.status = TemporaryFileStatus.SUCCESS;

const reader = new FileReader();
reader.onload = () => {
this.value = { src: reader.result as string };
};
reader.readAsDataURL(item.file);
const blobUrl = URL.createObjectURL(this.temporaryFile.file);
this.value = { src: blobUrl };

const uploaded = await upload;
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.temporaryFile = { temporaryUnique: item.temporaryUnique, file: item.file };
this.dispatchEvent(new UmbChangeEvent());
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.
} finally {
this.#uploadAbort = undefined;
}
}

Expand All @@ -175,55 +202,103 @@
}

override render() {
if (this.value.src && this._previewAlias) {
return this.#renderFile(this.value.src, this._previewAlias, this.temporaryFile?.file);
} else {
if (!this.temporaryFile && !this.value.src) {
return this.#renderDropzone();
}

return html`
${this.temporaryFile ? this.#renderUploader() : nothing}
${this.value.src && this._previewAlias ? this.#renderFile(this.value.src) : nothing}
`;
}

#renderDropzone() {
return html`
<uui-file-dropzone
@click=${this.#handleBrowse}
id="dropzone"
label="dropzone"
@change="${this.#onUpload}"
accept="${ifDefined(this._extensions?.join(', '))}">
<uui-button label=${this.localize.term('media_clickToUpload')} @click="${this.#handleBrowse}"></uui-button>
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>
`;
}

#renderFile(src: string, previewAlias: string, file?: File) {
if (!previewAlias) return 'An error occurred. No previewer found for the file type.';
#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>
`;
}

#renderFile(src: string) {
return html`
<div id="wrapper">
<div style="position:relative; display: flex; width: fit-content; max-width: 100%">
<div id="wrapperInner">
<umb-extension-slot
type="fileUploadPreview"
.props=${{ path: src, file: file }}
.filter=${(manifest: ManifestFileUploadPreview) => manifest.alias === previewAlias}>
.props=${{ path: src, file: this.temporaryFile?.file }}
.filter=${(manifest: ManifestFileUploadPreview) => manifest.alias === this._previewAlias}>
</umb-extension-slot>
${this.temporaryFile?.status === TemporaryFileStatus.WAITING
? html`<umb-temporary-file-badge></umb-temporary-file-badge>`
: nothing}
</div>
</div>
${this.#renderButtonRemove()}
`;
}

#renderButtonRemove() {
return html`<uui-button compact @click=${this.#handleRemove} label=${this.localize.term('content_uploadClear')}>
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
</uui-button>`;
return html`
<uui-button compact @click=${this.#handleRemove} label=${this.localize.term('content_uploadClear')}>
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
</uui-button>
`;
}

#handleRemove() {
this.value = { src: undefined };
this.temporaryFile = undefined;
this._progress = 0;
this.dispatchEvent(new UmbChangeEvent());

// If the upload promise happens to be in progress, cancel it.
this.#uploadAbort?.abort();
}

static override readonly styles = [
Expand All @@ -249,6 +324,45 @@
border-radius: var(--uui-border-radius);
}

#wrapperInner {
position: relative;
display: flex;
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;
Expand Down
Loading