diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts
index 9bfef4ee36..7bd699489b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts
@@ -43,12 +43,12 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement<
};
}
- #onFileDropped() {
- const data = this.dropzone?.getFiles()[0];
- if (!data) return;
+ #onUploadComplete() {
+ const data = this.dropzone?.getItems()[0];
+ if (!data?.temporaryFile) return;
- this.#temporaryUnique = data.temporaryUnique;
- this.#fileReader.readAsText(data.file);
+ this.#temporaryUnique = data.temporaryFile.temporaryUnique;
+ this.#fileReader.readAsText(data.temporaryFile.file);
}
async #onFileImport() {
@@ -136,7 +136,11 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement<
html`
Drag and drop your file here
-
+
`,
)}
`;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts
index 76e40e994f..06973dd3a2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts
@@ -33,19 +33,19 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<
this.#fileReader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const fileContent = e.target.result;
- this.#MediaTypePreviewBuilder(fileContent);
+ this.#mediaTypePreviewBuilder(fileContent);
} else {
this.#requestReset();
}
};
}
- #onFileDropped() {
- const data = this.dropzone?.getFiles()[0];
- if (!data) return;
+ #onUploadCompleted() {
+ const data = this.dropzone?.getItems()[0];
+ if (!data?.temporaryFile) return;
- this.#temporaryUnique = data.temporaryUnique;
- this.#fileReader.readAsText(data.file);
+ this.#temporaryUnique = data.temporaryFile.temporaryUnique;
+ this.#fileReader.readAsText(data.temporaryFile.file);
}
async #onFileImport() {
@@ -55,7 +55,7 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<
this._submitModal();
}
- #MediaTypePreviewBuilder(htmlString: string) {
+ #mediaTypePreviewBuilder(htmlString: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/xml');
const childNodes = doc.childNodes;
@@ -68,10 +68,10 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<
}
});
- this._fileContent = this.#MediaTypePreviewItemBuilder(elements);
+ this._fileContent = this.#mediaTypePreviewItemBuilder(elements);
}
- #MediaTypePreviewItemBuilder(elements: Array) {
+ #mediaTypePreviewItemBuilder(elements: Array) {
const mediaTypes: Array = [];
elements.forEach((MediaType) => {
const info = MediaType.getElementsByTagName('Info')[0];
@@ -129,7 +129,11 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<
html`
Drag and drop your file here
-
+
`,
)}
`;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts
index 86733391e0..2d1f125c04 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts
@@ -21,6 +21,10 @@ export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepo
}) {
return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take });
}
+
+ async requestMediaTypesOfFolders({ skip = 0, take = 100 } = {}) {
+ return this.#dataSource.getMediaTypesOfFolders({ skip, take });
+ }
}
export default UmbMediaTypeStructureRepository;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts
index df88ed9ec2..8990417133 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts
@@ -1,12 +1,10 @@
import type { UmbAllowedMediaTypeModel } from './types.js';
-import { UmbContentTypeStructureServerDataSourceBase } from '@umbraco-cms/backoffice/content-type';
-import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
-import type { AllowedMediaTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api';
+import { UmbContentTypeStructureServerDataSourceBase } from '@umbraco-cms/backoffice/content-type';
+import type { AllowedMediaTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
- *
-
* @class UmbMediaTypeStructureServerDataSource
* @augments {UmbContentTypeStructureServerDataSourceBase}
*/
@@ -21,6 +19,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu
getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) {
return getAllowedMediaTypesOfExtension({ fileExtension, skip, take });
}
+
+ getMediaTypesOfFolders({ skip, take }: { skip: number; take: number }) {
+ return getAllowedMediaTypesOfFolders({ skip, take });
+ }
}
const getAllowedChildrenOf = (unique: string | null) => {
@@ -42,6 +44,12 @@ const mapper = (item: AllowedMediaTypeModel): UmbAllowedMediaTypeModel => {
};
};
+const getAllowedMediaTypesOfFolders = async ({ skip, take }: { skip: number; take: number }) => {
+ // eslint-disable-next-line local-rules/no-direct-api-import
+ const { items } = await MediaTypeService.getItemMediaTypeFolders({ skip, take });
+ return items.map((item) => mapper(item));
+};
+
const getAllowedMediaTypesOfExtension = async ({
fileExtension,
skip,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts
index ab82bad8c0..91b6008b49 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts
@@ -1,13 +1,14 @@
-//TODO Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration()
+// TODO: Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration()
+
/**
- *
+ * @returns {string} The unique identifier for the Umbraco folder media-type.
*/
export function getUmbracoFolderUnique(): string {
return 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d';
}
/**
- *
- * @param unique
+ * @param {string} unique The unique identifier of the media-type to check.
+ * @returns {boolean} True if the unique identifier is the Umbraco folder media-type.
*/
export function isUmbracoFolder(unique?: string): boolean {
return unique === getUmbracoFolderUnique();
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 d830224a7c..091d168faa 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
@@ -4,8 +4,6 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js';
import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js';
import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection';
-import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event';
-
import './media-collection-toolbar.element.js';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
@@ -32,7 +30,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
});
}
- async #onChange() {
+ async #onComplete() {
this._progress = -1;
this.#mediaCollection?.requestCollection();
@@ -44,8 +42,11 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
eventContext.dispatchEvent(event);
}
- #onProgress(event: UmbProgressEvent) {
- this._progress = event.progress;
+ #onProgress(event: ProgressEvent) {
+ this._progress = (event.loaded / event.total) * 100;
+ if (this._progress >= 100) {
+ this._progress = -1;
+ }
}
protected override renderToolbar() {
@@ -54,7 +55,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
${when(this._progress >= 0, () => html``)}
`;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
index 4c54e71f50..b0a1e49e2e 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
@@ -3,7 +3,7 @@ import { UmbMediaItemRepository } from '../../repository/index.js';
import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js';
import type { UmbCropModel, UmbMediaPickerPropertyValue } from '../../types.js';
import type { UmbMediaItemModel } from '../../repository/index.js';
-import type { UmbUploadableFileModel } from '../../dropzone/index.js';
+import type { UmbUploadableItem } from '../../dropzone/types.js';
import { customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@@ -331,7 +331,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
}
async #onUploadCompleted(e: CustomEvent) {
- const completed = e.detail?.completed as Array;
+ const completed = e.detail as Array;
const uploaded = completed.map((file) => file.unique);
this.#addItems(uploaded);
}
@@ -346,7 +346,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
#renderDropzone() {
if (this.readonly) return nothing;
if (this._cards && this._cards.length >= this.max) return;
- return html``;
+ return html``;
}
#renderItems() {
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 2ee586305a..19a7896c1c 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
@@ -1,272 +1,328 @@
-import type { UmbMediaDetailModel } from '../types.js';
import { UmbMediaDetailRepository } from '../repository/index.js';
-import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js';
-import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import type { UmbMediaDetailModel, UmbMediaValueModel } from '../types.js';
+import { UmbFileDropzoneItemStatus } from './types.js';
+import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/index.js';
+import type {
+ UmbUploadableFile,
+ UmbUploadableFolder,
+ UmbFileDropzoneDroppedItems,
+ UmbFileDropzoneProgress,
+ UmbUploadableItem,
+ UmbAllowedMediaTypesOfExtension,
+ UmbAllowedChildrenOfMediaType,
+} from './types.js';
+import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
+import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
-import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type';
-import {
- TemporaryFileStatus,
- UmbTemporaryFileManager,
- type UmbTemporaryFileModel,
-} from '@umbraco-cms/backoffice/temporary-file';
import { UmbId } from '@umbraco-cms/backoffice/id';
+import { UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
-import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
-
-export interface UmbUploadableFileModel extends UmbTemporaryFileModel {
- unique: string;
- mediaTypeUnique: string;
-}
-
-export interface UmbUploadableExtensionModel {
- fileExtension: string;
- mediaTypes: Array;
-}
+import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
/**
- * Manages the dropzone and uploads files to the server.
- * @function createFilesAsMedia - Upload files to the server and creates the items using corresponding media type.
- * @function createFilesAsTemporary - Upload the files as temporary files and returns the data.
- * @observable completed - Emits an array of completed uploads.
+ * Manages the dropzone and uploads folders and files to the server.
+ * @function createMediaItems - Upload files and folders to the server and creates the items using corresponding media type.
+ * @function createTemporaryFiles - Upload the files as temporary files and returns the data.
+ * @observable progress - Emits the number of completed items and total items.
+ * @observable progressItems - Emits the items with their current status.
*/
export class UmbDropzoneManager extends UmbControllerBase {
- #host;
-
- #tempFileManager = new UmbTemporaryFileManager(this);
+ readonly #host: UmbControllerHost;
+ #isFoldersAllowed = true;
#mediaTypeStructure = new UmbMediaTypeStructureRepository(this);
#mediaDetailRepository = new UmbMediaDetailRepository(this);
- #completed = new UmbArrayState(
- [],
- (upload) => upload.temporaryUnique,
- );
- public readonly completed = this.#completed.asObservable();
+ #tempFileManager = new UmbTemporaryFileManager(this);
+
+ // The available media types for a file extension.
+ readonly #availableMediaTypesOf = new UmbArrayState([], (x) => x.extension);
+
+ // The media types that the parent will allow to be created under it.
+ readonly #allowedChildrenOf = new UmbArrayState([], (x) => x.mediaTypeUnique);
+
+ readonly #progress = new UmbObjectState({ total: 0, completed: 0 });
+ public readonly progress = this.#progress.asObservable();
+
+ readonly #progressItems = new UmbArrayState([], (x) => x.unique);
+ public readonly progressItems = this.#progressItems.asObservable();
constructor(host: UmbControllerHost) {
super(host);
this.#host = host;
}
+ public setIsFoldersAllowed(isAllowed: boolean) {
+ this.#isFoldersAllowed = isAllowed;
+ }
+
+ public getIsFoldersAllowed(): boolean {
+ return this.#isFoldersAllowed;
+ }
+
+ /** @deprecated Please use `createMediaItems()` instead; this method will be removed in Umbraco 17. */
+ public createFilesAsMedia = this.createMediaItems;
+
+ /**
+ * Uploads files and folders to the server and creates the media items with corresponding media type.\
+ * Allows the user to pick a media type option if multiple types are allowed.
+ * @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload
+ * @param {string | null} parentUnique - Where the items should be uploaded
+ */
+ public async createMediaItems(items: UmbFileDropzoneDroppedItems, parentUnique: string | null = null) {
+ const uploadableItems = await this.#setupProgress(items, parentUnique);
+ if (uploadableItems.length === 1) {
+ // When there is only one item being uploaded, allow the user to pick the media type, if more than one is allowed.
+ await this.#createOneMediaItem(uploadableItems[0]);
+ } else {
+ // When there are multiple items being uploaded, automatically pick the media types for each item. We probably want to allow the user to pick the media type in the future.
+ await this.#createMediaItems(uploadableItems);
+ }
+ }
+
+ /** @deprecated Please use `createTemporaryFiles()` instead; this method will be removed in Umbraco 17. */
+ public createFilesAsTemporary = this.createTemporaryFiles;
+
/**
* Uploads the files as temporary files and returns the data.
- * @param files
- * @returns Promise>
+ * @param { File[] } files - The files to upload.
+ * @returns {Promise>} - Files as temporary files.
*/
- public async createFilesAsTemporary(files: Array): Promise> {
- this.#completed.setValue([]);
- const temporaryFiles: Array = [];
+ public async createTemporaryFiles(files: Array) {
+ const uploadableItems = (await this.#setupProgress({ files, folders: [] }, null)) as Array;
- for (const file of files) {
- const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: UmbId.new(), file });
- this.#completed.setValue([...this.#completed.getValue(), uploaded]);
- temporaryFiles.push(uploaded);
- }
+ const uploadedItems: Array = [];
- return temporaryFiles;
- }
+ for (const item of uploadableItems) {
+ // Upload as temp file
+ const uploaded = await this.#tempFileManager.uploadOne({
+ temporaryUnique: item.temporaryFile.temporaryUnique,
+ file: item.temporaryFile.file,
+ });
- /**
- * Uploads files to the server and creates the items with corresponding media type.
- * Allows the user to pick a media type option if multiple types are allowed.
- * @param files
- * @param parentUnique
- * @returns Promise
- */
- public async createFilesAsMedia(files: Array, parentUnique: string | null) {
- if (!files.length) return;
- if (files.length === 1) return this.#handleOneOneFile(files[0], parentUnique);
+ // Update progress
+ const progress = this.#progress.getValue();
+ this.#progress.update({ completed: progress.completed + 1 });
- // Handler for multiple files dropped
-
- this.#completed.setValue([]);
- // removes duplicate file types so we don't call endpoints unnecessarily when building options.
- const mimeTypes = [...new Set(files.map((file) => file.type))];
- const optionsArray = await this.#buildOptionsArrayFrom(
- mimeTypes.map((mimetype) => this.#getExtensionFromMime(mimetype)),
- parentUnique,
- );
-
- if (!optionsArray.length) return; // None of the files are allowed in current dropzone.
-
- // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones?
- const uploadableFiles: Array = [];
- const notAllowedFiles: Array = [];
-
- for (const file of files) {
- const extension = this.#getExtensionFromMime(file.type);
- if (!extension) {
- // Folders have no extension on file drop. We assume it is a folder being uploaded.
- continue;
- }
- const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes;
-
- if (!options || !options.length) {
- // TODO Current dropped file not allowed in this area. Find a good way to show this to the user after we finish uploading the rest of the files.
- notAllowedFiles.push(file);
- continue;
+ if (uploaded.status === TemporaryFileStatus.SUCCESS) {
+ this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.COMPLETE });
+ } else {
+ this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.ERROR });
}
- // Since we are uploading multiple files, we will pick first allowed option.
- // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown?
- const mediaType = options[0];
- uploadableFiles.push({
- temporaryUnique: UmbId.new(),
- file,
- mediaTypeUnique: mediaType.unique,
- unique: UmbId.new(),
- });
+ // Add to return value
+ uploadedItems.push(uploaded);
}
- notAllowedFiles.forEach((file) => {
- // TODO: It seems like some implementation(user feedback) is missing here? [NL]
- console.error(`File ${file.name} of type ${file.type} is not allowed here.`);
- });
-
- if (!uploadableFiles.length) return;
-
- await this.#handleUpload(uploadableFiles, parentUnique);
- }
-
- async #handleOneOneFile(file: File, parentUnique: string | null) {
- this.#completed.setValue([]);
- const extension = this.#getExtensionFromMime(file.type);
-
- if (!extension) {
- // TODO Folders have no extension on file drop. Assume it is a folder being uploaded.
- return;
- }
-
- const optionsArray = await this.#buildOptionsArrayFrom([extension], parentUnique);
- if (!optionsArray.length || !optionsArray[0].mediaTypes.length) {
- throw new Error(`File ${file.name} of type ${file.type} is not allowed here.`); // Parent does not allow this file type here.
- }
-
- const mediaTypes = optionsArray[0].mediaTypes;
- if (mediaTypes.length === 1) {
- // Only one allowed option, upload file using that option.
- const uploadableFile: UmbUploadableFileModel = {
- unique: UmbId.new(),
- temporaryUnique: UmbId.new(),
- file,
- mediaTypeUnique: mediaTypes[0].unique,
- };
-
- await this.#handleUpload([uploadableFile], parentUnique);
- return;
- }
-
- // Multiple options, show a dialog for the user to pick one.
- const mediaType = await this.#showDialogMediaTypePicker(mediaTypes);
- if (!mediaType) return; // Upload cancelled.
-
- const uploadableFile: UmbUploadableFileModel = {
- unique: UmbId.new(),
- temporaryUnique: UmbId.new(),
- file,
- mediaTypeUnique: mediaType.unique,
- };
- await this.#handleUpload([uploadableFile], parentUnique);
- }
-
- #getExtensionFromMime(mime: string): string {
- //TODO Temporary solution.
- if (!mime) return ''; //folders
- const extension = mime.split('/')[1];
- switch (extension) {
- case 'svg+xml':
- return 'svg';
- default:
- return extension;
- }
- }
-
- async #buildOptionsArrayFrom(
- fileExtensions: Array,
- parentUnique: string | null,
- ): Promise> {
- let parentMediaType: string | null = null;
- if (parentUnique) {
- const { data } = await this.#mediaDetailRepository.requestByUnique(parentUnique);
- parentMediaType = data?.mediaType.unique ?? null;
- }
-
- // Getting all media types allowed in our current position based on parent's media type.
-
- const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(parentMediaType);
- if (!allAllowedMediaTypes?.items.length) return [];
-
- const allowedByParent = allAllowedMediaTypes.items;
-
- // Building an array of options the files can be uploaded as.
- const options: Array = [];
-
- for (const fileExtension of fileExtensions) {
- const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension });
- const mediaTypes = extensionOptions.filter((option) => {
- return allowedByParent.find((allowed) => option.unique === allowed.unique);
- });
- options.push({ fileExtension, mediaTypes });
- }
- return options;
+ return uploadedItems;
}
async #showDialogMediaTypePicker(options: Array) {
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } });
const value = await modalContext.onSubmit().catch(() => undefined);
- return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null;
+ return value?.mediaTypeUnique;
}
- async #handleUpload(files: Array, parentUnique: string | null) {
- for (const file of files) {
- const upload = (await this.#tempFileManager.uploadOne(file)) as UmbUploadableFileModel;
+ async #createOneMediaItem(item: UmbUploadableItem) {
+ const options = await this.#getMediaTypeOptions(item);
+ if (!options.length) {
+ return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
+ }
- if (upload.status === TemporaryFileStatus.SUCCESS) {
- // Upload successful. Create media item.
- // TODO: Use a scaffolding feature to ensure consistency. [NL]
- const preset: Partial = {
- unique: file.unique,
- mediaType: {
- unique: upload.mediaTypeUnique,
- collection: null,
- },
- variants: [
- {
- culture: null,
- segment: null,
- name: upload.file.name,
- createDate: null,
- updateDate: null,
- },
- ],
- values: [
- {
- // We do not need to parse the right editorAlias here, because the server does not read it. If we need to parse it we would need to load the contentType to make this happen properly. [NL]
- editorAlias: null as any,
- alias: 'umbracoFile',
- value: { temporaryFileId: upload.temporaryUnique },
- culture: null,
- segment: null,
- },
- ],
- };
- const { data } = await this.#mediaDetailRepository.createScaffold(preset);
- await this.#mediaDetailRepository.create(data!, parentUnique);
- }
- // TODO Find a good way to show files that ended up as TemporaryFileStatus.ERROR. Notice that they were allowed in current area
+ const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique;
- this.#completed.setValue([...this.#completed.getValue(), upload]);
+ if (!mediaTypeUnique) {
+ return this.#updateProgress(item, UmbFileDropzoneItemStatus.CANCELLED);
+ }
+
+ if (item.temporaryFile) {
+ this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
+ } else if (item.folder) {
+ this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique);
}
}
- private _reset() {
- //
+ async #createMediaItems(uploadableItems: Array) {
+ for (const item of uploadableItems) {
+ const options = await this.#getMediaTypeOptions(item);
+ if (!options.length) {
+ this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
+ continue;
+ }
+
+ const mediaTypeUnique = options[0].unique;
+
+ // Handle files and folders differently: a file is uploaded as temp then created as a media item, and a folder is created as a media item directly
+ if (item.temporaryFile) {
+ await this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
+ } else if (item.folder) {
+ await this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique);
+ }
+ }
}
+ async #handleFile(item: UmbUploadableFile, mediaTypeUnique: string) {
+ // 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);
+ return;
+ }
+
+ // Create the media item.
+ const scaffold = await this.#getItemScaffold(item, mediaTypeUnique);
+ const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique);
+
+ if (data) {
+ this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE);
+ } else {
+ this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR);
+ }
+ }
+
+ async #handleFolder(item: UmbUploadableFolder, mediaTypeUnique: string) {
+ const scaffold = await this.#getItemScaffold(item, mediaTypeUnique);
+ const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique);
+ if (data) {
+ this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE);
+ } else {
+ this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR);
+ }
+ }
+
+ async #uploadAsTemporaryFile(item: UmbUploadableFile) {
+ return await this.#tempFileManager.uploadOne({
+ temporaryUnique: item.temporaryFile.temporaryUnique,
+ file: item.temporaryFile.file,
+ });
+ }
+
+ // Media types
+ async #getMediaTypeOptions(item: UmbUploadableItem): Promise> {
+ // Check the parent which children media types are allowed
+ const parent = item.parentUnique ? await this.#mediaDetailRepository.requestByUnique(item.parentUnique) : null;
+ const allowedChildren = await this.#getAllowedChildrenOf(parent?.data?.mediaType.unique ?? null);
+
+ const extension = item.temporaryFile?.file.name.split('.').pop() ?? null;
+
+ // Check which media types allow the file's extension
+ const availableMediaType = await this.#getAvailableMediaTypesOf(extension);
+
+ if (!availableMediaType.length) return [];
+
+ const options = allowedChildren.filter((x) => availableMediaType.find((y) => y.unique === x.unique));
+ return options;
+ }
+
+ async #getAvailableMediaTypesOf(extension: string | null) {
+ // Check if we already have information on this file extension.
+ const available = this.#availableMediaTypesOf
+ .getValue()
+ .find((x) => x.extension === extension)?.availableMediaTypes;
+ if (available) return available;
+
+ // Request information on this file extension
+ const availableMediaTypes = extension
+ ? await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension: extension })
+ : await this.#mediaTypeStructure.requestMediaTypesOfFolders();
+
+ this.#availableMediaTypesOf.appendOne({ extension, availableMediaTypes });
+ return availableMediaTypes;
+ }
+
+ async #getAllowedChildrenOf(mediaTypeUnique: string | null) {
+ //Check if we already got information on this media type.
+ const allowed = this.#allowedChildrenOf
+ .getValue()
+ .find((x) => x.mediaTypeUnique === mediaTypeUnique)?.allowedChildren;
+ if (allowed) return allowed;
+
+ // Request information on this media type.
+ const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaTypeUnique);
+ if (!data) throw new Error('Parent media type does not exists');
+
+ this.#allowedChildrenOf.appendOne({ mediaTypeUnique, allowedChildren: data.items });
+ return data.items;
+ }
+
+ // Scaffold
+ async #getItemScaffold(item: UmbUploadableItem, mediaTypeUnique: string): Promise {
+ // TODO: Use a scaffolding feature to ensure consistency. [NL]
+ const name = item.temporaryFile ? item.temporaryFile.file.name : (item.folder?.name ?? '');
+ const umbracoFile: UmbMediaValueModel = {
+ editorAlias: null as any,
+ alias: 'umbracoFile',
+ value: { temporaryFileId: item.temporaryFile?.temporaryUnique },
+ culture: null,
+ segment: null,
+ };
+
+ const preset: Partial = {
+ unique: item.unique,
+ mediaType: { unique: mediaTypeUnique, collection: null },
+ variants: [{ culture: null, segment: null, createDate: null, updateDate: null, name }],
+ values: item.temporaryFile ? [umbracoFile] : undefined,
+ };
+ const { data } = await this.#mediaDetailRepository.createScaffold(preset);
+ return data!;
+ }
+
+ // Progress handling
+ async #setupProgress(items: UmbFileDropzoneDroppedItems, parent: string | null) {
+ const current = this.#progress.getValue();
+ const currentItems = this.#progressItems.getValue();
+
+ const uploadableItems = this.#prepareItemsAsUploadable({ folders: items.folders, files: items.files }, parent);
+
+ this.#progressItems.setValue([...currentItems, ...uploadableItems]);
+ this.#progress.setValue({ total: current.total + uploadableItems.length, completed: current.completed });
+
+ return uploadableItems;
+ }
+
+ #updateProgress(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) {
+ this.#progressItems.updateOne(item.unique, { status });
+ const progress = this.#progress.getValue();
+ this.#progress.update({ completed: progress.completed + 1 });
+ }
+
+ readonly #prepareItemsAsUploadable = (
+ { folders, files }: UmbFileDropzoneDroppedItems,
+ parentUnique: string | null,
+ ): Array => {
+ const items: Array = [];
+
+ for (const file of files) {
+ const unique = UmbId.new();
+ if (file.type) {
+ items.push({
+ unique,
+ parentUnique,
+ status: UmbFileDropzoneItemStatus.WAITING,
+ temporaryFile: { file, temporaryUnique: UmbId.new() },
+ });
+ }
+ }
+
+ for (const subfolder of folders) {
+ const unique = UmbId.new();
+ items.push({
+ unique,
+ parentUnique,
+ status: UmbFileDropzoneItemStatus.WAITING,
+ folder: { name: subfolder.folderName },
+ });
+
+ items.push(...this.#prepareItemsAsUploadable({ folders: subfolder.folders, files: subfolder.files }, unique));
+ }
+ return items;
+ };
+
public override destroy() {
this.#tempFileManager.destroy();
- this.#completed.destroy();
super.destroy();
}
}
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 f54bd1cbeb..7975f36562 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,8 @@
-import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js';
-import { UmbProgressEvent } from '@umbraco-cms/backoffice/event';
-import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
-import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
+import { UmbDropzoneManager } from './dropzone-manager.class.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 { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
+import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-dropzone')
export class UmbDropzoneElement extends UmbLitElement {
@@ -16,24 +15,47 @@ export class UmbDropzoneElement extends UmbLitElement {
@property({ type: Boolean })
createAsTemporary: boolean = false;
- @property({ type: Array, attribute: false })
- accept: Array = [];
+ @property({ type: String })
+ accept?: string;
- //TODO: logic to disable the dropzone?
+ @property({ type: Boolean, reflect: true })
+ disabled = false;
- #files: Array = [];
+ @property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true })
+ public get disableFolderUpload() {
+ return this._disableFolderUpload;
+ }
+ public set disableFolderUpload(isAllowed: boolean) {
+ this.dropzoneManager.setIsFoldersAllowed(!isAllowed);
+ }
+ private readonly _disableFolderUpload = false;
+ @state()
+ private _progressItems: Array = [];
+
+ public dropzoneManager: UmbDropzoneManager;
+
+ /**
+ * @deprecated Please use `getItems()` instead; this method will be removed in Umbraco 17.
+ * @returns {Array} An array of uploadable items.
+ */
public getFiles() {
- return this.#files;
+ return this.getItems();
+ }
+
+ public getItems() {
+ return this._progressItems;
}
public browse() {
+ if (this.disabled) return;
const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement;
return element.browse();
}
constructor() {
super();
+ this.dropzoneManager = new UmbDropzoneManager(this);
document.addEventListener('dragenter', this.#handleDragEnter.bind(this));
document.addEventListener('dragleave', this.#handleDragLeave.bind(this));
document.addEventListener('drop', this.#handleDrop.bind(this));
@@ -41,70 +63,73 @@ export class UmbDropzoneElement extends UmbLitElement {
override disconnectedCallback(): void {
super.disconnectedCallback();
+ this.dropzoneManager.destroy();
document.removeEventListener('dragenter', this.#handleDragEnter.bind(this));
document.removeEventListener('dragleave', this.#handleDragLeave.bind(this));
document.removeEventListener('drop', this.#handleDrop.bind(this));
}
#handleDragEnter(e: DragEvent) {
+ if (this.disabled) return;
// Avoid collision with UmbSorterController
const types = e.dataTransfer?.types;
if (!types?.length || !types?.includes('Files')) return;
+
this.toggleAttribute('dragging', true);
}
#handleDragLeave() {
+ if (this.disabled) return;
this.toggleAttribute('dragging', false);
}
#handleDrop(event: DragEvent) {
event.preventDefault();
+ if (this.disabled) return;
this.toggleAttribute('dragging', false);
}
async #onDropFiles(event: UUIFileDropzoneEvent) {
- // TODO Handle of folder uploads.
+ if (this.disabled) return;
+ if (!event.detail.files.length && !event.detail.folders.length) return;
- const files: Array = event.detail.files;
- if (!files.length) return;
+ // TODO Create some placeholder items while files are being uploaded? Could update them as they get completed.
+ // We can observe progressItems and check for any files that did not succeed, then show some kind of dialog to the user with the information.
- const dropzoneManager = new UmbDropzoneManager(this);
this.observe(
- dropzoneManager.completed,
- (completed) => {
- if (!completed.length) return;
-
- const progress = Math.floor(completed.length / files.length);
- this.dispatchEvent(new UmbProgressEvent(progress));
-
- if (completed.length === files.length) {
- this.#files = completed;
- this.dispatchEvent(new CustomEvent('change', { detail: { completed } }));
- dropzoneManager.destroy();
- }
- },
- '_observeCompleted',
+ this.dropzoneManager.progress,
+ (progress) =>
+ this.dispatchEvent(new ProgressEvent('progress', { loaded: progress.completed, total: progress.total })),
+ '_observeProgress',
);
- //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed.
+
+ this.observe(this.dropzoneManager.progressItems, (progressItems: Array) => {
+ this._progressItems = progressItems;
+ const waiting = progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING);
+ if (progressItems.length && !waiting) {
+ this.dispatchEvent(new CustomEvent('complete', { detail: progressItems }));
+ }
+ });
+
if (this.createAsTemporary) {
- await dropzoneManager.createFilesAsTemporary(files);
+ this.dropzoneManager.createTemporaryFiles(event.detail.files);
} else {
- await dropzoneManager.createFilesAsMedia(files, this.parentUnique);
+ this.dropzoneManager.createMediaItems(event.detail, this.parentUnique);
}
}
override render() {
return html``;
+ label=${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}>`;
}
static override styles = [
css`
- :host([dragging]) #dropzone {
+ :host(:not([disabled])[dragging]) #dropzone {
opacity: 1;
pointer-events: all;
}
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
new file mode 100644
index 0000000000..0e99dbb2d7
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts
@@ -0,0 +1,47 @@
+import type { UUIFileFolder } from '@umbraco-cms/backoffice/external/uui';
+import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type';
+import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
+
+export interface UmbFileDropzoneDroppedItems {
+ files: Array;
+ folders: Array;
+}
+
+export interface UmbUploadableItem {
+ unique: string;
+ parentUnique: string | null;
+ status: UmbFileDropzoneItemStatus;
+ folder?: { name: string };
+ temporaryFile?: UmbTemporaryFileModel;
+}
+
+export interface UmbUploadableFile extends UmbUploadableItem {
+ temporaryFile: UmbTemporaryFileModel;
+}
+
+export interface UmbUploadableFolder extends UmbUploadableItem {
+ folder: { name: string };
+}
+
+export interface UmbAllowedMediaTypesOfExtension {
+ extension: string | null; // Null is considered a folder.
+ availableMediaTypes: Array;
+}
+
+export interface UmbAllowedChildrenOfMediaType {
+ mediaTypeUnique: string | null;
+ allowedChildren: Array;
+}
+
+export interface UmbFileDropzoneProgress {
+ total: number;
+ completed: number;
+}
+
+export enum UmbFileDropzoneItemStatus {
+ WAITING = 'waiting',
+ COMPLETE = 'complete',
+ NOT_ALLOWED = 'not allowed',
+ CANCELLED = 'cancelled',
+ ERROR = 'error',
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
index b83579178f..c29baf210f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
@@ -168,7 +168,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
#renderBody() {
return html`${this.#renderToolbar()}
- this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}>
+ this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}>
${
!this._mediaFilteredList.length
? html`${this.localize.term('content_listViewNoItems')}
`