Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
990a1315ac | ||
|
|
a5f80ef42a | ||
|
|
51d34c050c | ||
|
|
e000b93ffa | ||
|
|
00357674c1 | ||
|
|
99f3bb4466 | ||
|
|
3872268ea7 | ||
|
|
6945b457a8 | ||
|
|
236d0be53e | ||
|
|
9149139a36 | ||
|
|
4211c962f3 | ||
|
|
35e9eebf33 | ||
|
|
e713883c84 | ||
|
|
91723945f0 | ||
|
|
908582d83a | ||
|
|
db9d47058f | ||
|
|
9b0ce81df0 | ||
|
|
67cf1e0e89 | ||
|
|
c7e74d0659 | ||
|
|
7574b36d2e | ||
|
|
662ce99238 | ||
|
|
2e7a6dedb0 | ||
|
|
12a817dc08 | ||
|
|
44601a5204 | ||
|
|
e404295151 | ||
|
|
d0c35e740f | ||
|
|
322d29b8ef | ||
|
|
ec5fd89c24 | ||
|
|
e217246d99 |
+7
@@ -4338,6 +4338,10 @@
|
||||
"resolved": "src/packages/documents",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@umbraco-backoffice/dropzone": {
|
||||
"resolved": "src/packages/dropzone",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@umbraco-backoffice/embedded-media": {
|
||||
"resolved": "src/packages/embedded-media",
|
||||
"link": true
|
||||
@@ -17701,6 +17705,9 @@
|
||||
"src/packages/documents": {
|
||||
"name": "@umbraco-backoffice/document"
|
||||
},
|
||||
"src/packages/dropzone": {
|
||||
"name": "@umbraco-backoffice/dropzone"
|
||||
},
|
||||
"src/packages/embedded-media": {
|
||||
"name": "@umbraco-backoffice/embedded-media"
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.js",
|
||||
"./document-type": "./dist-cms/packages/documents/document-types/index.js",
|
||||
"./document": "./dist-cms/packages/documents/documents/index.js",
|
||||
"./dropzone": "./dist-cms/packages/media/dropzone/index.js",
|
||||
"./dropzone": "./dist-cms/packages/dropzone/dropzone/index.js",
|
||||
"./entity-action": "./dist-cms/packages/core/entity-action/index.js",
|
||||
"./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js",
|
||||
"./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js",
|
||||
@@ -296,4 +296,4 @@
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-9
@@ -1,4 +1,4 @@
|
||||
import type { UmbUploadableItem } from '../../types.js';
|
||||
import type { UmbFileDropzoneDroppedItems, UmbUploadableItem } from '../../types.js';
|
||||
import { UmbFileDropzoneItemStatus } from '../../constants.js';
|
||||
import { UmbDropzoneManager } from '../../dropzone-manager.class.js';
|
||||
import { UmbDropzoneChangeEvent } from '../../dropzone-change.event.js';
|
||||
@@ -35,6 +35,12 @@ import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableItem[], typeof UmbLitElement>(
|
||||
UmbLitElement,
|
||||
) {
|
||||
/**
|
||||
* The parent of the uploaded items once they are moved from the temporary folder.
|
||||
*/
|
||||
@property({ attribute: 'parent-unique' })
|
||||
parentUnique: string | null = null;
|
||||
|
||||
/**
|
||||
* Comma-separated list of accepted mime types or file extensions.
|
||||
*/
|
||||
@@ -46,7 +52,7 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
|
||||
*/
|
||||
@property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true })
|
||||
public set disableFolderUpload(isAllowed: boolean) {
|
||||
this.#manager.setIsFoldersAllowed(!isAllowed);
|
||||
this._manager.setIsFoldersAllowed(!isAllowed);
|
||||
}
|
||||
public get disableFolderUpload() {
|
||||
return this._disableFolderUpload;
|
||||
@@ -78,22 +84,22 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
|
||||
@state()
|
||||
protected _progressItems: Array<UmbUploadableItem> = [];
|
||||
|
||||
#manager = new UmbDropzoneManager(this);
|
||||
protected _manager = new UmbDropzoneManager(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.observe(
|
||||
this.#manager.progress,
|
||||
this._manager.progress,
|
||||
(progress) =>
|
||||
this.dispatchEvent(new ProgressEvent('progress', { loaded: progress.completed, total: progress.total })),
|
||||
'_observeProgress',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#manager.progressItems,
|
||||
this._manager.progressItems,
|
||||
(progressItems) => {
|
||||
this._progressItems = [...progressItems];
|
||||
this._progressItems = progressItems;
|
||||
const waiting = this._progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING);
|
||||
if (this._progressItems.length && !waiting) {
|
||||
this.value = [...this._progressItems];
|
||||
@@ -106,7 +112,7 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#manager.destroy();
|
||||
this._manager.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +224,12 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
|
||||
if (this.disabled) return;
|
||||
if (!e.detail.files.length && !e.detail.folders.length) return;
|
||||
|
||||
const uploadables = this.#manager.createTemporaryFiles(e.detail.files);
|
||||
const droppedItems: UmbFileDropzoneDroppedItems = {
|
||||
files: e.detail.files,
|
||||
folders: e.detail.folders,
|
||||
};
|
||||
|
||||
const uploadables = this._manager.createTemporaryFiles(droppedItems, this.parentUnique);
|
||||
this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadables));
|
||||
}
|
||||
|
||||
@@ -233,7 +244,7 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
|
||||
}
|
||||
|
||||
#handleRemove() {
|
||||
this.#manager.removeAll();
|
||||
this._manager.removeAll();
|
||||
}
|
||||
|
||||
static override readonly styles = [
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
export { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js';
|
||||
|
||||
export enum UmbFileDropzoneItemStatus {
|
||||
WAITING = 'waiting',
|
||||
COMPLETE = 'complete',
|
||||
@@ -0,0 +1,196 @@
|
||||
import { UmbFileDropzoneItemStatus } from './constants.js';
|
||||
import type {
|
||||
UmbFileDropzoneDroppedItems,
|
||||
UmbFileDropzoneProgress,
|
||||
UmbUploadableFile,
|
||||
UmbUploadableFolder,
|
||||
UmbUploadableItem,
|
||||
} from './types.js';
|
||||
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import {
|
||||
TemporaryFileStatus,
|
||||
UmbTemporaryFileManager,
|
||||
type UmbTemporaryFileModel,
|
||||
} from '@umbraco-cms/backoffice/temporary-file';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @property {UmbObjectState<UmbFileDropzoneProgress>} progress - Emits the number of completed items and total items.
|
||||
* @property {UmbArrayState<UmbUploadableItem>} progressItems - Emits the items with their current status.
|
||||
*/
|
||||
export class UmbDropzoneManager extends UmbControllerBase {
|
||||
readonly #progress = new UmbObjectState<UmbFileDropzoneProgress>({ total: 0, completed: 0 });
|
||||
public readonly progress = this.#progress.asObservable();
|
||||
|
||||
readonly #progressItems = new UmbArrayState<UmbUploadableItem>([], (x) => x.unique);
|
||||
public readonly progressItems = this.#progressItems.asObservable();
|
||||
|
||||
#isFoldersAllowed = true;
|
||||
#tempFileManager = new UmbTemporaryFileManager(this);
|
||||
|
||||
public setIsFoldersAllowed(isAllowed: boolean) {
|
||||
this.#isFoldersAllowed = isAllowed;
|
||||
}
|
||||
|
||||
public getIsFoldersAllowed(): boolean {
|
||||
return this.#isFoldersAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the files as temporary files and returns the data.
|
||||
* @param {UmbFileDropzoneDroppedItems} items - The items to upload.
|
||||
* @param {string | null} parentUnique - The parent unique.
|
||||
* @returns {Promise<Array<UmbUploadableItem>>} - Files as temporary files.
|
||||
*/
|
||||
public async createTemporaryFiles(
|
||||
items: UmbFileDropzoneDroppedItems,
|
||||
parentUnique?: string | null,
|
||||
): Promise<Array<UmbUploadableItem>> {
|
||||
const uploadableItems = this.#setupProgress(items, parentUnique ?? null);
|
||||
|
||||
const uploadedItems: Array<UmbUploadableItem> = [];
|
||||
|
||||
for (const item of uploadableItems) {
|
||||
// Check if the item is a file
|
||||
if (this.#isUploadableFile(item)) {
|
||||
// Upload as temp file
|
||||
const uploaded = await this.#tempFileManager.uploadOne(item.temporaryFile);
|
||||
|
||||
// Update progress
|
||||
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
|
||||
this.updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE);
|
||||
} else {
|
||||
this.updateStatus(item, UmbFileDropzoneItemStatus.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to return value
|
||||
uploadedItems.push(item);
|
||||
}
|
||||
|
||||
return uploadedItems;
|
||||
}
|
||||
|
||||
public removeOne(item: UmbUploadableItem) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
this.#progressItems.removeOne(item.unique);
|
||||
if (item.temporaryFile) {
|
||||
this.#tempFileManager.removeOne(item.temporaryFile.temporaryUnique);
|
||||
}
|
||||
}
|
||||
|
||||
public remove(items: Array<UmbUploadableItem>) {
|
||||
const uniques: string[] = [];
|
||||
for (const item of items) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
if (item.temporaryFile) {
|
||||
uniques.push(item.temporaryFile.temporaryUnique);
|
||||
}
|
||||
}
|
||||
this.#progressItems.remove(uniques);
|
||||
const temporaryUniques = items.map((x) => x.temporaryFile?.temporaryUnique).filter((x): x is string => !!x);
|
||||
this.#tempFileManager.remove(temporaryUniques);
|
||||
}
|
||||
|
||||
public removeAll() {
|
||||
for (const item of this.#progressItems.getValue()) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
}
|
||||
this.#progressItems.setValue([]);
|
||||
this.#tempFileManager.removeAll();
|
||||
}
|
||||
|
||||
// Progress handling
|
||||
#setupProgress(items: UmbFileDropzoneDroppedItems, parent: string | null) {
|
||||
const current = this.#progress.getValue();
|
||||
|
||||
const uploadableItems = this.#prepareItemsAsUploadable(items, parent);
|
||||
|
||||
this.#progressItems.append(uploadableItems);
|
||||
console.log(
|
||||
'trying to append',
|
||||
uploadableItems,
|
||||
'to',
|
||||
this.#progressItems,
|
||||
'which now has the value of',
|
||||
this.#progressItems.getValue(),
|
||||
);
|
||||
this.#progress.update({
|
||||
total: current.total + uploadableItems.length,
|
||||
});
|
||||
|
||||
return uploadableItems;
|
||||
}
|
||||
|
||||
updateStatus(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) {
|
||||
this.#progressItems.updateOne(item.unique, { status });
|
||||
const progress = this.#progress.getValue();
|
||||
this.#progress.update({ completed: progress.completed + 1 });
|
||||
}
|
||||
|
||||
#isUploadableFile(item: UmbUploadableItem): item is UmbUploadableFile {
|
||||
return 'temporaryFile' in item && item.temporaryFile !== undefined;
|
||||
}
|
||||
|
||||
#updateProgress(item: UmbUploadableItem, progress: number) {
|
||||
this.#progressItems.updateOne(item.unique, { progress });
|
||||
}
|
||||
|
||||
#prepareItemsAsUploadable = (
|
||||
{ folders, files }: UmbFileDropzoneDroppedItems,
|
||||
parentUnique: string | null,
|
||||
): Array<UmbUploadableItem> => {
|
||||
const items: Array<UmbUploadableItem> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const temporaryFile: UmbTemporaryFileModel = {
|
||||
file,
|
||||
temporaryUnique: UmbId.new(),
|
||||
abortController: new AbortController(),
|
||||
onProgress: (progress) => this.#updateProgress(uploadableItem, progress),
|
||||
};
|
||||
|
||||
const uploadableItem: UmbUploadableFile = {
|
||||
unique: UmbId.new(),
|
||||
parentUnique,
|
||||
status: UmbFileDropzoneItemStatus.WAITING,
|
||||
progress: 0,
|
||||
temporaryFile,
|
||||
};
|
||||
|
||||
temporaryFile.abortController?.signal.addEventListener('abort', () => {
|
||||
this.updateStatus(uploadableItem, UmbFileDropzoneItemStatus.CANCELLED);
|
||||
});
|
||||
|
||||
items.push(uploadableItem);
|
||||
}
|
||||
|
||||
if (!this.getIsFoldersAllowed()) {
|
||||
return items;
|
||||
}
|
||||
|
||||
for (const subfolder of folders) {
|
||||
const unique = UmbId.new();
|
||||
items.push({
|
||||
unique,
|
||||
parentUnique,
|
||||
status: UmbFileDropzoneItemStatus.WAITING,
|
||||
progress: 100, // Folders are created instantly.
|
||||
folder: { name: subfolder.folderName },
|
||||
} satisfies UmbUploadableFolder);
|
||||
|
||||
items.push(...this.#prepareItemsAsUploadable({ folders: subfolder.folders, files: subfolder.files }, unique));
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
public override destroy() {
|
||||
this.#tempFileManager.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
export * from './constants.js';
|
||||
export * from './components/index.js';
|
||||
export * from './modals/index.js';
|
||||
export * from './dropzone-manager.class.js';
|
||||
export * from './dropzone-submitted.event.js';
|
||||
export * from './dropzone-change.event.js';
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@umbraco-backoffice/dropzone",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const name = 'Umbraco.Core.Dropzone';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { rmSync } from 'fs';
|
||||
import { getDefaultConfig } from '../../vite-config-base';
|
||||
|
||||
const dist = '../../../dist-cms/packages/dropzone';
|
||||
|
||||
// delete the unbundled dist folder
|
||||
rmSync(dist, { recursive: true, force: true });
|
||||
|
||||
export default defineConfig({
|
||||
...getDefaultConfig({
|
||||
dist,
|
||||
entry: {
|
||||
'dropzone/index': 'dropzone/index.ts',
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -1,396 +0,0 @@
|
||||
import { UmbMediaDetailRepository } from '../media/repository/index.js';
|
||||
import type { UmbMediaDetailModel, UmbMediaValueModel } from '../media/types.js';
|
||||
import { UmbFileDropzoneItemStatus } from './constants.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,
|
||||
type UmbTemporaryFileModel,
|
||||
} from '@umbraco-cms/backoffice/temporary-file';
|
||||
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
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 type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
readonly #host: UmbControllerHost;
|
||||
#isFoldersAllowed = true;
|
||||
|
||||
#mediaTypeStructure = new UmbMediaTypeStructureRepository(this);
|
||||
#mediaDetailRepository = new UmbMediaDetailRepository(this);
|
||||
|
||||
#tempFileManager = new UmbTemporaryFileManager(this);
|
||||
|
||||
// The available media types for a file extension.
|
||||
readonly #availableMediaTypesOf = new UmbArrayState<UmbAllowedMediaTypesOfExtension>([], (x) => x.extension);
|
||||
|
||||
// The media types that the parent will allow to be created under it.
|
||||
readonly #allowedChildrenOf = new UmbArrayState<UmbAllowedChildrenOfMediaType>([], (x) => x.mediaTypeUnique);
|
||||
|
||||
readonly #progress = new UmbObjectState<UmbFileDropzoneProgress>({ total: 0, completed: 0 });
|
||||
public readonly progress = this.#progress.asObservable();
|
||||
|
||||
readonly #progressItems = new UmbArrayState<UmbUploadableItem>([], (x) => x.unique);
|
||||
public readonly progressItems = this.#progressItems.asObservable();
|
||||
|
||||
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
|
||||
#localization = new UmbLocalizationController(this);
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
this.#host = host;
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
|
||||
this.#notificationContext = context;
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
* @deprecated Use the {@link UmbDropzoneMediaManager} class instead. This will be removed in Umbraco 18.
|
||||
* @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload.
|
||||
* @param {string | null} parentUnique - Where the items should be uploaded.
|
||||
* @returns {Array<UmbUploadableItem>} - The items about to be uploaded.
|
||||
*/
|
||||
public createMediaItems(items: UmbFileDropzoneDroppedItems, parentUnique: string | null = null) {
|
||||
const uploadableItems = this.#setupProgress(items, parentUnique);
|
||||
|
||||
if (!uploadableItems.length) return [];
|
||||
|
||||
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.
|
||||
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.
|
||||
this.#createMediaItems(uploadableItems);
|
||||
}
|
||||
|
||||
return 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 { File[] } files - The files to upload.
|
||||
* @returns {Promise<Array<UmbUploadableItem>>} - Files as temporary files.
|
||||
*/
|
||||
public async createTemporaryFiles(files: Array<File>): Promise<Array<UmbUploadableItem>> {
|
||||
const uploadableItems = this.#setupProgress({ files, folders: [] }, null) as Array<UmbUploadableFile>;
|
||||
|
||||
const uploadedItems: Array<UmbUploadableItem> = [];
|
||||
|
||||
for (const item of uploadableItems) {
|
||||
// Upload as temp file
|
||||
const uploaded = await this.#tempFileManager.uploadOne(item.temporaryFile);
|
||||
|
||||
// Update progress
|
||||
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
|
||||
this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE);
|
||||
} else {
|
||||
this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR);
|
||||
}
|
||||
|
||||
// Add to return value
|
||||
uploadedItems.push(item);
|
||||
}
|
||||
|
||||
return uploadedItems;
|
||||
}
|
||||
|
||||
public removeOne(item: UmbUploadableItem) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
this.#progressItems.removeOne(item.unique);
|
||||
if (item.temporaryFile) {
|
||||
this.#tempFileManager.removeOne(item.temporaryFile.temporaryUnique);
|
||||
}
|
||||
}
|
||||
|
||||
public remove(items: Array<UmbUploadableItem>) {
|
||||
const uniques: string[] = [];
|
||||
for (const item of items) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
if (item.temporaryFile) {
|
||||
uniques.push(item.temporaryFile.temporaryUnique);
|
||||
}
|
||||
}
|
||||
this.#progressItems.remove(uniques);
|
||||
const temporaryUniques = items.map((x) => x.temporaryFile?.temporaryUnique).filter((x): x is string => !!x);
|
||||
this.#tempFileManager.remove(temporaryUniques);
|
||||
}
|
||||
|
||||
public removeAll() {
|
||||
for (const item of this.#progressItems.getValue()) {
|
||||
item.temporaryFile?.abortController?.abort();
|
||||
}
|
||||
this.#progressItems.setValue([]);
|
||||
this.#tempFileManager.removeAll();
|
||||
}
|
||||
|
||||
async #showDialogMediaTypePicker(options: Array<UmbAllowedMediaTypeModel>) {
|
||||
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?.mediaTypeUnique;
|
||||
}
|
||||
|
||||
async #createOneMediaItem(item: UmbUploadableItem) {
|
||||
const options = await this.#getMediaTypeOptions(item);
|
||||
if (!options.length) {
|
||||
this.#notificationContext?.peek('warning', {
|
||||
data: {
|
||||
message: `${this.#localization.term('media_disallowedFileType')}: ${item.temporaryFile?.file.name}.`,
|
||||
},
|
||||
});
|
||||
return this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
|
||||
}
|
||||
|
||||
const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique;
|
||||
|
||||
if (!mediaTypeUnique) {
|
||||
return this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED);
|
||||
}
|
||||
|
||||
if (item.temporaryFile) {
|
||||
this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
|
||||
} else if (item.folder) {
|
||||
this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique);
|
||||
}
|
||||
}
|
||||
|
||||
async #createMediaItems(uploadableItems: Array<UmbUploadableItem>) {
|
||||
for (const item of uploadableItems) {
|
||||
const options = await this.#getMediaTypeOptions(item);
|
||||
if (!options.length) {
|
||||
this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mediaTypeUnique = options[0].unique;
|
||||
|
||||
if (!mediaTypeUnique) {
|
||||
throw new Error('Media type unique is not defined');
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
|
||||
} else if (item.folder) {
|
||||
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.CANCELLED) {
|
||||
this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED);
|
||||
return;
|
||||
}
|
||||
if (temporaryFile.status !== TemporaryFileStatus.SUCCESS) {
|
||||
this.#updateStatus(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.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE);
|
||||
} else {
|
||||
this.#updateStatus(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.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE);
|
||||
} else {
|
||||
this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
const parent = item.parentUnique ? await this.#mediaDetailRepository.requestByUnique(item.parentUnique) : null;
|
||||
const allowedChildren = await this.#getAllowedChildrenOf(parent?.data?.mediaType.unique ?? null, item.parentUnique);
|
||||
|
||||
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, parentUnique: 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, parentUnique);
|
||||
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<UmbMediaDetailModel> {
|
||||
// TODO: Use a scaffolding feature to ensure consistency. [NL]
|
||||
const name = item.temporaryFile ? item.temporaryFile.file.name : (item.folder?.name ?? '');
|
||||
const umbracoFile: UmbMediaValueModel = {
|
||||
editorAlias: '',
|
||||
alias: 'umbracoFile',
|
||||
value: { temporaryFileId: item.temporaryFile?.temporaryUnique },
|
||||
culture: null,
|
||||
segment: null,
|
||||
};
|
||||
|
||||
const preset: Partial<UmbMediaDetailModel> = {
|
||||
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
|
||||
#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;
|
||||
}
|
||||
|
||||
#updateStatus(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) {
|
||||
this.#progressItems.updateOne(item.unique, { status });
|
||||
const progress = this.#progress.getValue();
|
||||
this.#progress.update({ completed: progress.completed + 1 });
|
||||
}
|
||||
|
||||
#updateProgress(item: UmbUploadableItem, progress: number) {
|
||||
this.#progressItems.updateOne(item.unique, { progress });
|
||||
}
|
||||
|
||||
readonly #prepareItemsAsUploadable = (
|
||||
{ folders, files }: UmbFileDropzoneDroppedItems,
|
||||
parentUnique: string | null,
|
||||
): Array<UmbUploadableItem> => {
|
||||
const items: Array<UmbUploadableItem> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const temporaryFile: UmbTemporaryFileModel = {
|
||||
file,
|
||||
temporaryUnique: UmbId.new(),
|
||||
abortController: new AbortController(),
|
||||
onProgress: (progress) => this.#updateProgress(uploadableItem, progress),
|
||||
};
|
||||
|
||||
const uploadableItem: UmbUploadableFile = {
|
||||
unique: UmbId.new(),
|
||||
parentUnique,
|
||||
status: UmbFileDropzoneItemStatus.WAITING,
|
||||
progress: 0,
|
||||
temporaryFile,
|
||||
};
|
||||
|
||||
temporaryFile.abortController?.signal.addEventListener('abort', () => {
|
||||
this.#updateStatus(uploadableItem, UmbFileDropzoneItemStatus.CANCELLED);
|
||||
});
|
||||
|
||||
items.push(uploadableItem);
|
||||
}
|
||||
|
||||
for (const subfolder of folders) {
|
||||
const unique = UmbId.new();
|
||||
items.push({
|
||||
unique,
|
||||
parentUnique,
|
||||
status: UmbFileDropzoneItemStatus.WAITING,
|
||||
progress: 100, // Folders are created instantly.
|
||||
folder: { name: subfolder.folderName },
|
||||
});
|
||||
|
||||
items.push(...this.#prepareItemsAsUploadable({ folders: subfolder.folders, files: subfolder.files }, unique));
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
public override destroy() {
|
||||
this.#tempFileManager.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { manifests as modalManifests } from './modals/manifests.js';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest> = [...modalManifests];
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dropzone-media-type-picker/index.js';
|
||||
@@ -2,7 +2,6 @@ import { manifests as mediaManifests } from './media/manifests.js';
|
||||
import { manifests as mediaSectionManifests } from './media-section/manifests.js';
|
||||
import { manifests as mediaTypesManifests } from './media-types/manifests.js';
|
||||
import { manifests as imagingManifests } from './imaging/manifests.js';
|
||||
import { manifests as dropzoneManifests } from './dropzone/manifests.js';
|
||||
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [
|
||||
@@ -10,5 +9,4 @@ export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> =
|
||||
...mediaManifests,
|
||||
...mediaTypesManifests,
|
||||
...imagingManifests,
|
||||
...dropzoneManifests,
|
||||
];
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ import type { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone
|
||||
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
|
||||
UmbMediaCollectionItemModel,
|
||||
UmbMediaCollectionFilterModel
|
||||
|
||||
+1
-1
@@ -7,9 +7,9 @@ import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone';
|
||||
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
|
||||
|
||||
import '@umbraco-cms/backoffice/imaging';
|
||||
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
|
||||
|
||||
@customElement('umb-media-grid-collection-view')
|
||||
export class UmbMediaGridCollectionViewElement extends UmbLitElement {
|
||||
|
||||
+22
-12
@@ -1,7 +1,8 @@
|
||||
import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js';
|
||||
import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js';
|
||||
import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/constants.js';
|
||||
import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone';
|
||||
import type { UmbDropzoneMediaElement } from '../../dropzone/dropzone-media.element.js';
|
||||
import type { UmbDropzoneChangeEvent } from '@umbraco-cms/backoffice/dropzone';
|
||||
import { css, 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';
|
||||
@@ -178,8 +179,17 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.observe(this.#itemManager.items, () => {
|
||||
this.#populateCards();
|
||||
this.observe(
|
||||
this.#itemManager.items,
|
||||
(items) => {
|
||||
console.log('items', items);
|
||||
this.#populateCards();
|
||||
},
|
||||
'_observeItems',
|
||||
);
|
||||
|
||||
this.observe(this.#itemManager.uniques, (uniques) => {
|
||||
console.log('[Debug] did we get new uniques?', uniques);
|
||||
});
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_IMAGE_CROPPER_EDITOR_MODAL)
|
||||
@@ -338,10 +348,13 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
async #onUploadCompleted(e: CustomEvent) {
|
||||
const completed = e.detail as Array<UmbUploadableItem>;
|
||||
const uploaded = completed.map((file) => file.unique);
|
||||
this.#addItems(uploaded);
|
||||
async #onUpload(e: UmbDropzoneChangeEvent) {
|
||||
const target = e.target as UmbDropzoneMediaElement;
|
||||
const completed = target.value;
|
||||
const uploaded = completed?.map((file) => file.unique);
|
||||
if (uploaded?.length) {
|
||||
this.#addItems(uploaded);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -352,11 +365,8 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
|
||||
}
|
||||
|
||||
#renderDropzone() {
|
||||
if (this.readonly) return nothing;
|
||||
if (this._cards && this._cards.length >= this.max) return;
|
||||
return html`<umb-dropzone-media
|
||||
?multiple=${this.max > 1}
|
||||
@complete=${this.#onUploadCompleted}></umb-dropzone-media>`;
|
||||
if (this.readonly || this._cards?.length >= this.max) return nothing;
|
||||
return html`<umb-dropzone-media ?multiple=${this.max > 1} @change=${this.#onUpload}></umb-dropzone-media>`;
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
|
||||
+195
-10
@@ -1,21 +1,206 @@
|
||||
import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from '../constants.js';
|
||||
import { UmbMediaDetailRepository } from '../repository/detail/index.js';
|
||||
import type { UmbMediaDetailModel, UmbMediaValueModel } from '../types.js';
|
||||
|
||||
import {
|
||||
UmbDropzoneManager,
|
||||
type UmbFileDropzoneDroppedItems,
|
||||
UmbFileDropzoneItemStatus,
|
||||
type UmbDropzoneManager,
|
||||
type UmbAllowedChildrenOfMediaType,
|
||||
type UmbAllowedMediaTypesOfExtension,
|
||||
type UmbUploadableFile,
|
||||
type UmbUploadableFolder,
|
||||
type UmbUploadableItem,
|
||||
} from '@umbraco-cms/backoffice/dropzone';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
|
||||
import { UmbMediaTypeStructureRepository, type UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbDropzoneMediaManager extends UmbControllerBase {
|
||||
// The available media types for a file extension.
|
||||
readonly #availableMediaTypesOf = new UmbArrayState<UmbAllowedMediaTypesOfExtension>([], (x) => x.extension);
|
||||
|
||||
// The media types that the parent will allow to be created under it.
|
||||
readonly #allowedChildrenOf = new UmbArrayState<UmbAllowedChildrenOfMediaType>([], (x) => x.mediaTypeUnique);
|
||||
|
||||
readonly #mediaTypeStructure = new UmbMediaTypeStructureRepository(this);
|
||||
readonly #mediaDetailRepository = new UmbMediaDetailRepository(this);
|
||||
readonly #localization = new UmbLocalizationController(this);
|
||||
readonly #dropzoneManager;
|
||||
|
||||
constructor(host: UmbControllerHost, dropzoneManager: UmbDropzoneManager) {
|
||||
super(host);
|
||||
this.#dropzoneManager = dropzoneManager;
|
||||
}
|
||||
|
||||
export class UmbDropzoneMediaManager extends UmbDropzoneManager {
|
||||
/**
|
||||
* 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.
|
||||
* @param {Array<UmbUploadableItem>} uploadableItems - The files and folders to upload.
|
||||
* @returns {Array<UmbUploadableItem>} - The items about to be uploaded.
|
||||
*/
|
||||
public override createMediaItems(
|
||||
items: UmbFileDropzoneDroppedItems,
|
||||
parentUnique: string | null,
|
||||
): Array<UmbUploadableItem> {
|
||||
return super.createMediaItems(items, parentUnique);
|
||||
public createMediaItems(uploadableItems: Array<UmbUploadableItem>): Array<UmbUploadableItem> {
|
||||
if (!uploadableItems.length) return [];
|
||||
|
||||
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.
|
||||
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.
|
||||
this.#createMediaItems(uploadableItems);
|
||||
}
|
||||
|
||||
return uploadableItems;
|
||||
}
|
||||
|
||||
async #showDialogMediaTypePicker(options: Array<UmbAllowedMediaTypeModel>) {
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
const modalContext = modalManager.open(this, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } });
|
||||
const value = await modalContext.onSubmit().catch(() => undefined);
|
||||
return value?.mediaTypeUnique;
|
||||
}
|
||||
|
||||
async #createOneMediaItem(item: UmbUploadableItem) {
|
||||
const options = await this.#getMediaTypeOptions(item);
|
||||
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
|
||||
if (!options.length) {
|
||||
notificationContext?.peek('warning', {
|
||||
data: {
|
||||
message: `${this.#localization.term('media_disallowedFileType')}: ${item.temporaryFile?.file.name}.`,
|
||||
},
|
||||
});
|
||||
this.#dropzoneManager.updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique;
|
||||
|
||||
if (!mediaTypeUnique) {
|
||||
this.#dropzoneManager.updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.temporaryFile) {
|
||||
this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
|
||||
} else if (item.folder) {
|
||||
this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique);
|
||||
}
|
||||
}
|
||||
|
||||
async #createMediaItems(uploadableItems: Array<UmbUploadableItem>) {
|
||||
for (const item of uploadableItems) {
|
||||
const options = await this.#getMediaTypeOptions(item);
|
||||
if (!options.length) {
|
||||
this.#dropzoneManager.updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mediaTypeUnique = options[0].unique;
|
||||
|
||||
if (!mediaTypeUnique) {
|
||||
throw new Error('Media type unique is not defined');
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.#handleFile(item as UmbUploadableFile, mediaTypeUnique);
|
||||
} else if (item.folder) {
|
||||
this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #handleFile(item: UmbUploadableFile, mediaTypeUnique: string) {
|
||||
// Create the media item.
|
||||
const scaffold = await this.#getItemScaffold(item, mediaTypeUnique);
|
||||
const { error } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique);
|
||||
|
||||
this.#dropzoneManager.updateStatus(
|
||||
item,
|
||||
error ? UmbFileDropzoneItemStatus.ERROR : UmbFileDropzoneItemStatus.COMPLETE,
|
||||
);
|
||||
}
|
||||
|
||||
async #handleFolder(item: UmbUploadableFolder, mediaTypeUnique: string) {
|
||||
const scaffold = await this.#getItemScaffold(item, mediaTypeUnique);
|
||||
const { error } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique);
|
||||
|
||||
this.#dropzoneManager.updateStatus(
|
||||
item,
|
||||
error ? UmbFileDropzoneItemStatus.ERROR : UmbFileDropzoneItemStatus.COMPLETE,
|
||||
);
|
||||
}
|
||||
|
||||
// Media types
|
||||
async #getMediaTypeOptions(item: UmbUploadableItem): Promise<Array<UmbAllowedMediaTypeModel>> {
|
||||
// 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, item.parentUnique);
|
||||
|
||||
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, parentUnique: 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, parentUnique);
|
||||
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<UmbMediaDetailModel> {
|
||||
// TODO: Use a scaffolding feature to ensure consistency. [NL]
|
||||
const name = item.temporaryFile ? item.temporaryFile.file.name : (item.folder?.name ?? '');
|
||||
const umbracoFile: UmbMediaValueModel = {
|
||||
editorAlias: '',
|
||||
alias: 'umbracoFile',
|
||||
value: { temporaryFileId: item.temporaryFile?.temporaryUnique },
|
||||
culture: null,
|
||||
segment: null,
|
||||
};
|
||||
|
||||
const preset: Partial<UmbMediaDetailModel> = {
|
||||
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!;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-32
@@ -4,8 +4,9 @@ import {
|
||||
UmbFileDropzoneItemStatus,
|
||||
UmbDropzoneSubmittedEvent,
|
||||
type UmbUploadableItem,
|
||||
type UmbFileDropzoneDroppedItems,
|
||||
} from '@umbraco-cms/backoffice/dropzone';
|
||||
import { css, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
/**
|
||||
@@ -19,24 +20,6 @@ import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'
|
||||
*/
|
||||
@customElement('umb-dropzone-media')
|
||||
export class UmbDropzoneMediaElement extends UmbInputDropzoneElement {
|
||||
@property({ attribute: 'parent-unique' })
|
||||
parentUnique: string | null = null;
|
||||
|
||||
/**
|
||||
* Determines if the dropzone should create temporary files or media items directly.
|
||||
* @deprecated Use the {@link UmbInputDropzoneElement} instead.
|
||||
*/
|
||||
@property({ type: Boolean, attribute: 'create-as-temporary' })
|
||||
createAsTemporary: boolean = false;
|
||||
|
||||
#dropzoneManager = new UmbDropzoneMediaManager(this);
|
||||
|
||||
/**
|
||||
* @deprecated Please use `getItems()` instead; this method will be removed in Umbraco 17.
|
||||
* @returns {Array<UmbUploadableItem>} An array of uploadable items.
|
||||
*/
|
||||
public getFiles = this.getItems;
|
||||
|
||||
/**
|
||||
* Gets the current value of the uploaded items.
|
||||
* @returns {Array<UmbUploadableItem>} An array of uploadable items.
|
||||
@@ -45,8 +28,10 @@ export class UmbDropzoneMediaElement extends UmbInputDropzoneElement {
|
||||
return this._progressItems;
|
||||
}
|
||||
|
||||
public progressItems = () => this.#dropzoneManager.progressItems;
|
||||
public progress = () => this.#dropzoneManager.progress;
|
||||
public progressItems = () => this._manager.progressItems;
|
||||
public progress = () => this._manager.progress;
|
||||
|
||||
#mediaManager = new UmbDropzoneMediaManager(this, this._manager);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -56,36 +41,38 @@ export class UmbDropzoneMediaElement extends UmbInputDropzoneElement {
|
||||
document.addEventListener('drop', this.#handleDrop.bind(this));
|
||||
|
||||
this.observe(
|
||||
this.#dropzoneManager.progressItems,
|
||||
(progressItems: Array<UmbUploadableItem>) => {
|
||||
this._manager.progressItems,
|
||||
(progressItems) => {
|
||||
const waiting = progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING);
|
||||
if (progressItems.length && !waiting) {
|
||||
this.dispatchEvent(new CustomEvent('complete', { detail: progressItems }));
|
||||
}
|
||||
},
|
||||
'_observeProgressItems',
|
||||
'_observeProgressItemsComplete',
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
override async onUpload(event: UUIFileDropzoneEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (this.disabled) return;
|
||||
if (!event.detail.files.length && !event.detail.folders.length) return;
|
||||
|
||||
if (this.createAsTemporary) {
|
||||
const uploadable = this.#dropzoneManager.createTemporaryFiles(event.detail.files);
|
||||
this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadable));
|
||||
} else {
|
||||
const uploadable = this.#dropzoneManager.createMediaItems(event.detail, this.parentUnique);
|
||||
this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadable));
|
||||
}
|
||||
const droppedItems: UmbFileDropzoneDroppedItems = {
|
||||
files: event.detail.files,
|
||||
folders: event.detail.folders,
|
||||
};
|
||||
|
||||
const uploadableItems = await this._manager.createTemporaryFiles(droppedItems, this.parentUnique);
|
||||
const uploadables = this.#mediaManager.createMediaItems(uploadableItems);
|
||||
this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadables));
|
||||
}
|
||||
|
||||
#handleDragEnter(e: DragEvent) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './dropzone-media-type-picker/index.js';
|
||||
export { UMB_IMAGE_CROPPER_EDITOR_MODAL } from './image-cropper-editor/index.js';
|
||||
export * from './media-caption-alt-text/constants.js';
|
||||
export { UMB_MEDIA_PICKER_MODAL } from './media-picker/index.js';
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.Dropzone.MediaTypePicker',
|
||||
name: 'Dropzone Media Type Picker Modal',
|
||||
element: () => import('./dropzone-media-type-picker/dropzone-media-type-picker-modal.element.js'),
|
||||
element: () => import('./dropzone-media-type-picker-modal.element.js'),
|
||||
},
|
||||
];
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './dropzone-media-type-picker/index.js';
|
||||
export * from './image-cropper-editor/index.js';
|
||||
export * from './media-caption-alt-text/index.js';
|
||||
export * from './media-picker/index.js';
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { manifests as dropzoneMediaTypePickerManifests } from './dropzone-media-type-picker/manifests.js';
|
||||
import { manifests as imageCropperEditorManifests } from './image-cropper-editor/manifests.js';
|
||||
import { manifests as mediaCaptionAltTextManifests } from './media-caption-alt-text/manifests.js';
|
||||
import { manifests as mediaPickerManifests } from './media-picker/manifests.js';
|
||||
|
||||
export const manifests = [...imageCropperEditorManifests, ...mediaCaptionAltTextManifests, ...mediaPickerManifests];
|
||||
export const manifests = [
|
||||
...dropzoneMediaTypePickerManifests,
|
||||
...imageCropperEditorManifests,
|
||||
...mediaCaptionAltTextManifests,
|
||||
...mediaPickerManifests,
|
||||
];
|
||||
|
||||
@@ -13,7 +13,6 @@ export default defineConfig({
|
||||
entry: {
|
||||
'entry-point': 'entry-point.ts',
|
||||
'imaging/index': 'imaging/index.ts',
|
||||
'dropzone/index': 'dropzone/index.ts',
|
||||
'media-types/index': 'media-types/index.ts',
|
||||
'media/index': 'media/index.ts',
|
||||
'umbraco-package': 'umbraco-package.ts',
|
||||
|
||||
@@ -68,7 +68,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
|
||||
"@umbraco-cms/backoffice/document-blueprint": ["./src/packages/documents/document-blueprints/index.ts"],
|
||||
"@umbraco-cms/backoffice/document-type": ["./src/packages/documents/document-types/index.ts"],
|
||||
"@umbraco-cms/backoffice/document": ["./src/packages/documents/documents/index.ts"],
|
||||
"@umbraco-cms/backoffice/dropzone": ["./src/packages/media/dropzone/index.ts"],
|
||||
"@umbraco-cms/backoffice/dropzone": ["./src/packages/dropzone/dropzone/index.ts"],
|
||||
"@umbraco-cms/backoffice/entity-action": ["./src/packages/core/entity-action/index.ts"],
|
||||
"@umbraco-cms/backoffice/entity-bulk-action": ["./src/packages/core/entity-bulk-action/index.ts"],
|
||||
"@umbraco-cms/backoffice/entity-create-option-action": [
|
||||
|
||||
Reference in New Issue
Block a user