dropzone management

This commit is contained in:
Lone Iversen
2024-04-30 13:21:28 +02:00
parent 30141d787d
commit 4716c8021e
16 changed files with 691 additions and 94 deletions

View File

@@ -20,6 +20,7 @@
"element-internals-polyfill": "^1.3.10",
"lit": "^3.1.2",
"marked": "^12.0.0",
"mime-types": "^2.1.35",
"monaco-editor": "^0.46.0",
"rxjs": "^7.8.1",
"tinymce": "^6.8.3",
@@ -15849,7 +15850,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -15858,7 +15858,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},

View File

@@ -96,6 +96,7 @@
"./external/dompurify": "./dist-cms/external/dompurify/index.js",
"./external/lit": "./dist-cms/external/lit/index.js",
"./external/marked": "./dist-cms/external/marked/index.js",
"./external/mime-types": "./dist-cms/external/mime-types/index.js",
"./external/monaco-editor": "./dist-cms/external/monaco-editor/index.js",
"./external/openid": "./dist-cms/external/openid/index.js",
"./external/router-slot": "./dist-cms/external/router-slot/index.js",
@@ -179,6 +180,7 @@
"element-internals-polyfill": "^1.3.10",
"lit": "^3.1.2",
"marked": "^12.0.0",
"mime-types": "^2.1.35",
"monaco-editor": "^0.46.0",
"rxjs": "^7.8.1",
"tinymce": "^6.8.3",

View File

@@ -625,8 +625,7 @@ updater?: string | null
};
export type DocumentConfigurationResponseModel = {
sanitizeTinyMce: boolean
disableDeleteWhenReferenced: boolean
disableDeleteWhenReferenced: boolean
disableUnpublishWhenReferenced: boolean
allowEditInvariantFromNonDefault: boolean
allowNonExistingSegmentsCreation: boolean
@@ -1124,7 +1123,6 @@ mediaType: MediaTypeCollectionReferenceResponseModel
export type MediaConfigurationResponseModel = {
disableDeleteWhenReferenced: boolean
disableUnpublishWhenReferenced: boolean
sanitizeTinyMce: boolean
reservedFieldNames: Array<string>
};
@@ -1788,6 +1786,11 @@ export type PagedSearcherResponseModel = {
items: Array<SearcherResponseModel>
};
export type PagedSegmentResponseModel = {
total: number
items: Array<SegmentResponseModel>
};
export type PagedTagResponseModel = {
total: number
items: Array<TagResponseModel>
@@ -1894,6 +1897,13 @@ memberUserNames: Array<string>
memberGroupNames: Array<string>
};
export type PublicAccessResponseModel = {
loginDocument: ReferenceByIdModel
errorDocument: ReferenceByIdModel
members: Array<MemberItemResponseModel>
groups: Array<MemberGroupItemResponseModel>
};
export type PublishDocumentRequestModel = {
publishSchedules: Array<CultureAndScheduleRequestModel>
};
@@ -2052,6 +2062,11 @@ export type SecurityConfigurationResponseModel = {
passwordConfiguration: PasswordConfigurationResponseModel
};
export type SegmentResponseModel = {
name: string
alias: string
};
export type ServerConfigurationItemResponseModel = {
name: string
data: string
@@ -3397,7 +3412,7 @@ take?: number
,PutDocumentByIdNotifications: string
,PostDocumentByIdPublicAccess: string
,DeleteDocumentByIdPublicAccess: string
,GetDocumentByIdPublicAccess: void
,GetDocumentByIdPublicAccess: PublicAccessResponseModel
,PutDocumentByIdPublicAccess: string
,PutDocumentByIdPublish: string
,PutDocumentByIdPublishWithDescendants: string
@@ -3583,6 +3598,7 @@ requestBody?: UpdateLanguageRequestModel
responses: {
GetItemLanguage: Array<LanguageItemResponseModel>
,GetItemLanguageDefault: LanguageItemResponseModel
,GetLanguage: PagedLanguageResponseModel
,PostLanguage: string
,GetLanguageByIsoCode: LanguageResponseModel
@@ -3681,6 +3697,12 @@ export type MediaTypeData = {
GetItemMediaType: {
id?: Array<string>
};
GetItemMediaTypeAllowed: {
fileExtension?: string
skip?: number
take?: number
};
GetItemMediaTypeSearch: {
query?: string
@@ -3773,6 +3795,7 @@ take?: number
responses: {
GetItemMediaType: Array<MediaTypeItemResponseModel>
,GetItemMediaTypeAllowed: PagedModelMediaTypeItemResponseModel
,GetItemMediaTypeSearch: PagedModelMediaTypeItemResponseModel
,PostMediaType: string
,GetMediaTypeById: MediaTypeResponseModel
@@ -3996,10 +4019,10 @@ take?: number
responses: {
GetItemMemberGroup: Array<MemberGroupItemResponseModel>
,GetMemberGroup: PagedMemberGroupResponseModel
,PostMemberGroup: MemberGroupResponseModel
,PostMemberGroup: string
,GetMemberGroupById: MemberGroupResponseModel
,DeleteMemberGroupById: string
,PutMemberGroupById: MemberGroupResponseModel
,PutMemberGroupById: string
,GetTreeMemberGroupRoot: PagedNamedEntityTreeItemResponseModel
}
@@ -4591,6 +4614,24 @@ PostSecurityForgotPasswordVerify: {
}
export type SegmentData = {
payloads: {
GetSegment: {
skip?: number
take?: number
};
}
responses: {
GetSegment: PagedSegmentResponseModel
}
}
export type ServerData = {
@@ -4879,10 +4920,6 @@ export type UserDataData = {
PostUserData: {
requestBody?: CreateUserDataRequestModel
};
PutUserData: {
requestBody?: UpdateUserDataRequestModel
};
GetUserData: {
groups?: Array<string>
@@ -4890,6 +4927,10 @@ identifiers?: Array<string>
skip?: number
take?: number
};
PutUserData: {
requestBody?: UpdateUserDataRequestModel
};
GetUserDataById: {
id: string
@@ -4900,8 +4941,8 @@ GetUserDataById: {
responses: {
PostUserData: string
,PutUserData: string
,GetUserData: PagedUserDataResponseModel
,PutUserData: string
,GetUserDataById: UserDataModel
}
@@ -5152,7 +5193,11 @@ PostUserUnlock: {
export type WebhookData = {
payloads: {
GetWebhook: {
GetItemWebhook: {
id?: Array<string>
};
GetWebhook: {
skip?: number
take?: number
@@ -5173,21 +5218,17 @@ requestBody?: UpdateWebhookRequestModel
DeleteWebhookById: {
id: string
};
GetWebhookItem: {
ids?: Array<string>
};
}
responses: {
GetWebhook: PagedWebhookResponseModel
GetItemWebhook: Array<WebhookItemResponseModel>
,GetWebhook: PagedWebhookResponseModel
,PostWebhook: string
,GetWebhookById: WebhookResponseModel
,PutWebhookById: string
,DeleteWebhookById: string
,GetWebhookItem: Array<WebhookItemResponseModel>
}

View File

@@ -1,7 +1,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { AuditLogData, CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models';
import type { AuditLogData, CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models';
export class AuditLogService {
@@ -2220,6 +2220,7 @@ requestBody
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getDocumentByIdPublicAccess(data: DocumentData['payloads']['GetDocumentByIdPublicAccess']): CancelablePromise<DocumentData['responses']['GetDocumentByIdPublicAccess']> {
@@ -3129,6 +3130,22 @@ export class LanguageService {
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getItemLanguageDefault(): CancelablePromise<LanguageData['responses']['GetItemLanguageDefault']> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/item/language/default',
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -3546,6 +3563,30 @@ export class MediaTypeService {
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getItemMediaTypeAllowed(data: MediaTypeData['payloads']['GetItemMediaTypeAllowed'] = {}): CancelablePromise<MediaTypeData['responses']['GetItemMediaTypeAllowed']> {
const {
fileExtension,
skip,
take
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/item/media-type/allowed',
query: {
fileExtension, skip, take
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -4679,7 +4720,7 @@ take
}
/**
* @returns unknown Success
* @returns string Created
* @throws ApiError
*/
public static postMemberGroup(data: MemberGroupData['payloads']['PostMemberGroup'] = {}): CancelablePromise<MemberGroupData['responses']['PostMemberGroup']> {
@@ -4692,6 +4733,7 @@ take
url: '/umbraco/management/api/v1/member-group',
body: requestBody,
mediaType: 'application/json',
responseHeader: 'Umb-Generated-Resource',
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
@@ -4749,7 +4791,7 @@ take
}
/**
* @returns unknown Success
* @returns string Success
* @throws ApiError
*/
public static putMemberGroupById(data: MemberGroupData['payloads']['PutMemberGroupById']): CancelablePromise<MemberGroupData['responses']['PutMemberGroupById']> {
@@ -4766,6 +4808,7 @@ requestBody
},
body: requestBody,
mediaType: 'application/json',
responseHeader: 'Umb-Notifications',
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
@@ -6756,6 +6799,34 @@ export class SecurityService {
}
export class SegmentService {
/**
* @returns unknown Success
* @throws ApiError
*/
public static getSegment(data: SegmentData['payloads']['GetSegment'] = {}): CancelablePromise<SegmentData['responses']['GetSegment']> {
const {
skip,
take
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/segment',
query: {
skip, take
},
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
}
export class ServerService {
/**
@@ -7726,29 +7797,6 @@ export class UserDataService {
});
}
/**
* @returns string Success
* @throws ApiError
*/
public static putUserData(data: UserDataData['payloads']['PutUserData'] = {}): CancelablePromise<UserDataData['responses']['PutUserData']> {
const {
requestBody
} = data;
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/user-data',
body: requestBody,
mediaType: 'application/json',
responseHeader: 'Umb-Notifications',
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
404: `Not Found`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -7773,6 +7821,29 @@ take
});
}
/**
* @returns string Success
* @throws ApiError
*/
public static putUserData(data: UserDataData['payloads']['PutUserData'] = {}): CancelablePromise<UserDataData['responses']['PutUserData']> {
const {
requestBody
} = data;
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/user-data',
body: requestBody,
mediaType: 'application/json',
responseHeader: 'Umb-Notifications',
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
404: `Not Found`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -8829,6 +8900,28 @@ requestBody
export class WebhookService {
/**
* @returns unknown Success
* @throws ApiError
*/
public static getItemWebhook(data: WebhookData['payloads']['GetItemWebhook'] = {}): CancelablePromise<WebhookData['responses']['GetItemWebhook']> {
const {
id
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/item/webhook',
query: {
id
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -8950,26 +9043,4 @@ requestBody
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getWebhookItem(data: WebhookData['payloads']['GetWebhookItem'] = {}): CancelablePromise<WebhookData['responses']['GetWebhookItem']> {
const {
ids
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/webhook/item',
query: {
ids
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
}

View File

@@ -0,0 +1 @@
export * as mime from 'mime-types';

View File

@@ -4,8 +4,22 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type';
export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepositoryBase<UmbAllowedMediaTypeModel> {
#dataSource;
constructor(host: UmbControllerHost) {
super(host, UmbMediaTypeStructureServerDataSource);
this.#dataSource = new UmbMediaTypeStructureServerDataSource(host);
}
async requestMediaTypesOf({
fileExtension,
skip = 0,
take = 100,
}: {
fileExtension: string;
skip?: number;
take?: number;
}) {
return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take });
}
}

View File

@@ -17,6 +17,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu
constructor(host: UmbControllerHost) {
super(host, { getAllowedChildrenOf, mapper });
}
getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) {
return getAllowedMediaTypesOfExtension({ fileExtension, skip, take });
}
}
const getAllowedChildrenOf = (unique: string | null) => {
@@ -37,3 +41,17 @@ const mapper = (item: AllowedMediaTypeModel): UmbAllowedMediaTypeModel => {
icon: item.icon || null,
};
};
const getAllowedMediaTypesOfExtension = async ({
fileExtension,
skip,
take,
}: {
fileExtension: string;
skip: number;
take: number;
}) => {
// eslint-disable-next-line local-rules/no-direct-api-import
const { items } = await MediaTypeService.getItemMediaTypeAllowed({ fileExtension, skip, take });
return items.map((item) => mapper(item));
};

View File

@@ -1,28 +1,7 @@
export enum UmbMediaTypeFileType {
SVG = 'Vector Graphics (SVG)',
IMAGE = 'Image',
AUDIO = 'Audio',
VIDEO = 'Video',
ARTICLE = 'Article',
FILE = 'File',
}
import { mime } from '@umbraco-cms/backoffice/external/mime-types';
export function getMediaTypeByFileExtension(extension: string) {
if (extension === 'svg') return UmbMediaTypeFileType.SVG;
if (['jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff', 'tif', 'webp'].includes(extension))
return UmbMediaTypeFileType.IMAGE;
if (['mp3', 'weba', 'oga', 'opus'].includes(extension)) return UmbMediaTypeFileType.AUDIO;
if (['mp4', 'webm', 'ogv'].includes(extension)) return UmbMediaTypeFileType.VIDEO;
if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE;
return UmbMediaTypeFileType.FILE;
}
export function getMediaTypeByFileMimeType(mimetype: string) {
if (mimetype === 'image/svg+xml') return UmbMediaTypeFileType.SVG;
const [type, extension] = mimetype.split('/');
if (type === 'image') return UmbMediaTypeFileType.IMAGE;
if (type === 'audio') return UmbMediaTypeFileType.AUDIO;
if (type === 'video') return UmbMediaTypeFileType.VIDEO;
if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE;
return UmbMediaTypeFileType.FILE;
export function getExtensionFromMime(mimeType: string): string | undefined {
const extension = mime.extension(mimeType);
if (!extension) return; // extension doesn't exist.
return extension;
}

View File

@@ -0,0 +1,46 @@
import type { UmbContentTypeUploadableStructureRepositoryBase } from './repository/content-type-uploadable-structure-repository-base.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbMediaTypeItemModel } from '@umbraco-cms/backoffice/media-type';
import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
export class UmbDropzoneManager<T extends UmbMediaTypeItemModel = UmbMediaTypeItemModel> extends UmbControllerBase {
#init!: Promise<unknown>;
#fileManager = new UmbTemporaryFileManager(this);
#repository: UmbContentTypeUploadableStructureRepositoryBase<T>;
#parentUnique: string | null = null;
constructor(
host: UmbControllerHost,
typeRepository: UmbContentTypeUploadableStructureRepositoryBase<T>,
parentUnique: string | null,
) {
super(host);
this.#repository = typeRepository;
this.#parentUnique = parentUnique;
}
public async dropOneFile(file: File) {
const matchingMediaTypes = await this.#repository.requestAllowedMediaTypesOf(file.type);
//const options = this.#allowedMediaTypes.filter((allowedMediaType) => matchingMediaTypes.includes(allowedMediaType));
}
public async dropFiles(files: Array<File>) {}
async #requestAllowedMediaTypesOf(fileExtension: string) {
const { data } = await this.#repository.requestAllowedMediaTypesOf(fileExtension);
//const mediaTypes = data?.filter((option) => this.#allowedMediaTypes.includes(option));
//return { fileExtension, mediaTypes };
}
private _reset() {
//
}
public destroy() {
super.destroy();
}
}

View File

@@ -0,0 +1,302 @@
import { UmbMediaDetailRepository } from '../../repository/index.js';
import type { UmbMediaDetailModel } from '../../types.js';
import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import {
type UmbAllowedMediaTypeModel,
UmbMediaTypeStructureRepository,
UmbMediaTypeDetailRepository,
getExtensionFromMime,
} from '@umbraco-cms/backoffice/media-type';
import {
UmbTemporaryFileManager,
type UmbTemporaryFileQueueModel,
type UmbTemporaryFileModel,
} from '@umbraco-cms/backoffice/temporary-file';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
interface MediaTypeOptions {
fileExtension: string;
mediaTypes: Array<UmbAllowedMediaTypeModel>;
}
interface UploadableFile {
file: File;
mediaType: UmbAllowedMediaTypeModel;
regularUploadField?: boolean;
temporaryUnique?: string;
}
@customElement('umb-dropzone')
export class UmbDropzoneElement extends UmbLitElement {
#fileManager = new UmbTemporaryFileManager(this);
#mediaTypeStructure = new UmbMediaTypeStructureRepository(this);
#mediaDetailRepository = new UmbMediaDetailRepository(this);
#mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this);
#allowedMediaTypes: Array<UmbAllowedMediaTypeModel> = [];
@state()
private queue: Array<UmbTemporaryFileModel> = [];
@property({ attribute: false })
parentUnique: string | null = null;
public browse() {
const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement;
return element.browse();
}
async #getAllowedMediaTypes(): Promise<UmbAllowedMediaTypeModel[]> {
const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(this.parentUnique);
return data?.items ?? [];
}
async #getAllowedMediaTypesOf(fileExtension: string): Promise<MediaTypeOptions> {
const options = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension });
const mediaTypes = options.filter((option) => this.#allowedMediaTypes.includes(option));
return { fileExtension, mediaTypes };
}
constructor() {
super();
document.addEventListener('dragenter', this.#handleDragEnter.bind(this));
document.addEventListener('dragleave', this.#handleDragLeave.bind(this));
document.addEventListener('drop', this.#handleDrop.bind(this));
}
disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener('dragenter', this.#handleDragEnter.bind(this));
document.removeEventListener('dragleave', this.#handleDragLeave.bind(this));
document.removeEventListener('drop', this.#handleDrop.bind(this));
}
#handleDragEnter() {
this.toggleAttribute('dragging', true);
}
#handleDragLeave() {
this.toggleAttribute('dragging', false);
}
#handleDrop(event: DragEvent) {
event.preventDefault();
this.toggleAttribute('dragging', false);
}
async #onDropFiles(event: UUIFileDropzoneEvent) {
// TODO Handle of folder uploads.
const files: Array<File> = event.detail.files;
if (!files.length) return;
this.#allowedMediaTypes = await this.#getAllowedMediaTypes();
if (!this.#allowedMediaTypes.length) return;
// If we have files that are not allowed to be uploaded, we show those in a dialog to the user?
if (files.length === 1) {
this.#handleOneFile(files[0]);
} else {
this.#handleMultipleFiles(files);
}
}
async #handleOneFile(file: File) {
const extension = getExtensionFromMime(file.type);
if (!extension) return; // Extension doesn't exist.
const options = await this.#getAllowedMediaTypesOf(extension);
if (!options.mediaTypes.length) return; // File type not allowed in current dropzone.
if (options.mediaTypes.length === 1) {
this.#uploadFiles([{ file, mediaType: options.mediaTypes[0] }]);
return;
}
// Multiple options, show a dialog to the user to pick one.
//TODO: Implement dialog.
}
async #handleMultipleFiles(files: Array<File>) {
// removes duplicate file types so we don't call the endpoint unnecessarily for every file.
const types = [...new Set(files.map<string>((file) => file.type))];
const options: Array<MediaTypeOptions> = [];
for (const type of types) {
const extension = getExtensionFromMime(type);
if (!extension) return; // Extension doesn't exist.
options.push(await this.#getAllowedMediaTypesOf(extension));
}
// We are just going to automatically pick the first possible media type option for now, but consider an option dialog in the future.
const uploadable: Array<UploadableFile> = [];
files.forEach((file) => {
const mediaType = options.find((option) => option.fileExtension === file.type)?.mediaTypes[0] ?? undefined;
if (mediaType) uploadable.push({ file, mediaType });
});
this.#uploadFiles(uploadable);
}
async #uploadFiles(uploadable: Array<UploadableFile>) {
const queue = uploadable.map(
(item): UmbTemporaryFileQueueModel => ({ file: item.file, unique: item.temporaryUnique }),
);
const uploaded = await this.#fileManager.upload(queue);
for (const upload of uploaded) {
const mediaType = uploadable.find((item) => item.temporaryUnique === upload.unique)?.mediaType;
const value = mediaType?.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique;
if (!mediaType) return;
const preset: Partial<UmbMediaDetailModel> = {
mediaType: {
unique: mediaType.unique,
collection: null,
},
variants: [
{
culture: null,
segment: null,
name: upload.file.name,
createDate: null,
updateDate: null,
},
],
values: upload.file.type
? [
{
alias: 'umbracoFile',
value,
culture: null,
segment: null,
},
]
: [],
};
const { data } = await this.#mediaDetailRepository.createScaffold(preset);
if (!data) return;
await this.#mediaDetailRepository.create(data, this.parentUnique);
this.dispatchEvent(new UmbChangeEvent());
}
}
async #uploadHandler(files: Array<File>) {
//TODO: Folders uploaded via UUIDropzone are always empty. Investigate why.
const folders = files.filter((item) => !item.type).map((file): UmbTemporaryFileQueueModel => ({ file }));
const mediaItems = files.filter((item) => item.type);
const queue = mediaItems.map((file): UmbTemporaryFileQueueModel => ({ file }));
const uploaded = await this.#fileManager.upload(queue);
return [...folders, ...uploaded];
}
async #onFileUpload(event: UUIFileDropzoneEvent) {
const files: Array<File> = event.detail.files;
if (!files.length) return;
const uploads = await this.#uploadHandler(files);
for (const upload of uploads) {
const mediaType = /*this.#getMediaTypeFromMime(upload.file.type); */ '' as any;
const value = mediaType.unique === UmbMediaTypeFileType.IMAGE ? { src: upload.unique } : upload.unique;
const preset: Partial<UmbMediaDetailModel> = {
mediaType: {
unique: mediaType.unique,
collection: null,
},
variants: [
{
culture: null,
segment: null,
name: upload.file.name,
createDate: null,
updateDate: null,
},
],
values: upload.file.type
? [
{
alias: 'umbracoFile',
value,
culture: null,
segment: null,
},
]
: [],
};
const { data } = await this.#mediaDetailRepository.createScaffold(preset);
if (!data) return;
await this.#mediaDetailRepository.create(data, this.parentUnique);
this.dispatchEvent(new UmbChangeEvent());
}
}
render() {
return html`<uui-file-dropzone
id="dropzone"
multiple
@change=${this.#onFileUpload}
label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}"
accept=""></uui-file-dropzone>`;
}
static styles = [
css`
:host([dragging]) #dropzone {
opacity: 1;
pointer-events: all;
}
[dropzone] {
opacity: 0;
}
#dropzone {
opacity: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
inset: 0px;
z-index: 100;
backdrop-filter: opacity(1); /* Removes the built in blur effect */
border-radius: var(--uui-border-radius);
overflow: clip;
border: 1px solid var(--uui-color-focus);
}
#dropzone:after {
content: '';
display: block;
position: absolute;
inset: 0;
border-radius: var(--uui-border-radius);
background-color: var(--uui-color-focus);
opacity: 0.2;
}
`,
];
}
export default UmbDropzoneElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dropzone': UmbDropzoneElement;
}
}

View File

@@ -0,0 +1,2 @@
export * from './dropzone.element.js';
export * from './dropzone-manager.class.js';

View File

@@ -0,0 +1,16 @@
import type {
UmbContentTypeStructureDataSource,
UmbContentTypeStructureDataSourceConstructor,
} from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbContentTypeUploadableStructureDataSourceConstructor<ItemUploadableType>
extends UmbContentTypeStructureDataSourceConstructor<ItemUploadableType> {
new (host: UmbControllerHost): UmbContentTypeUploadableStructureDataSource<ItemUploadableType>;
}
export interface UmbContentTypeUploadableStructureDataSource<ItemUploadableType>
extends UmbContentTypeStructureDataSource<ItemUploadableType> {
getAllowedMediaTypesOf(fileExtension: string | null): Promise<UmbDataSourceResponse<ItemUploadableType>>;
}

View File

@@ -0,0 +1,32 @@
import type {
UmbContentTypeUploadableStructureDataSource,
UmbContentTypeUploadableStructureDataSourceConstructor,
} from './content-type-uploadable-structure-data-source.interface.js';
import type { UmbContentTypeUploadableStructureRepository } from './content-type-uploadable-structure-repository.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type';
export abstract class UmbContentTypeUploadableStructureRepositoryBase<ItemUploadableType>
extends UmbContentTypeStructureRepositoryBase<ItemUploadableType>
implements UmbContentTypeUploadableStructureRepository<ItemUploadableType>
{
#structureSource: UmbContentTypeUploadableStructureDataSource<ItemUploadableType>;
constructor(
host: UmbControllerHost,
structureSource: UmbContentTypeUploadableStructureDataSourceConstructor<ItemUploadableType>,
) {
super(host, structureSource);
this.#structureSource = new structureSource(host);
}
/**
* Returns a promise with the allowed media-types of a uploadable content type.
* @param {string} unique
* @return {*}
* @memberof UmbContentTypeUploadableStructureRepositoryBase
*/
requestAllowedMediaTypesOf(fileExtension: string | null) {
return this.#structureSource.getAllowedMediaTypesOf(fileExtension);
}
}

View File

@@ -0,0 +1,7 @@
import type { UmbContentTypeStructureRepository } from '@umbraco-cms/backoffice/content-type';
import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbContentTypeUploadableStructureRepository<ItemUploadableType>
extends UmbContentTypeStructureRepository<ItemUploadableType> {
requestAllowedMediaTypesOf(unique: string): Promise<UmbDataSourceResponse<ItemUploadableType>>;
}

View File

@@ -0,0 +1,66 @@
import {
type UmbContentTypeStructureDataSource,
UmbContentTypeStructureServerDataSourceBase,
type UmbContentTypeStructureServerDataSourceBaseArgs,
} from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
// Temp type
type AllowedContentTypeModel = {
id: string;
name: string;
description?: string | null;
icon?: string | null;
};
export interface UmbContentTypeUploadableStructureServerDataSourceBaseArgs<
ServerItemType extends AllowedContentTypeModel,
ClientItemType extends { unique: string },
> extends UmbContentTypeStructureServerDataSourceBaseArgs<ServerItemType, ClientItemType> {
getAllowedMediaTypesOf: (fileExtension: string | null) => Promise<ServerItemType>;
}
export abstract class UmbContentTypeUploadableStructureServerDataSourceBase<
ServerItemType extends AllowedContentTypeModel,
ClientItemType extends { unique: string },
>
extends UmbContentTypeStructureServerDataSourceBase<ServerItemType, ClientItemType>
implements UmbContentTypeStructureDataSource<ClientItemType>
{
#host;
#getAllowedChildrenOf;
#mapper;
/**
* Creates an instance of UmbContentTypeStructureServerDataSourceBase.
* @param {UmbControllerHost} host
* @memberof UmbItemServerDataSourceBase
*/
constructor(
host: UmbControllerHost,
args: UmbContentTypeUploadableStructureServerDataSourceBaseArgs<ServerItemType, ClientItemType>,
) {
super(host, args);
this.#host = host;
this.#getAllowedChildrenOf = args.getAllowedChildrenOf;
this.#mapper = args.mapper;
}
/**
* Returns a promise with the allowed content types for the given unique
* @param {string} unique
* @return {*}
* @memberof UmbContentTypeStructureServerDataSourceBase
*/
async getAllowedMediaTypesOf(fileExtension: string | null) {
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAllowedChildrenOf(fileExtension));
if (data) {
const items = data.items.map((item) => this.#mapper(item));
return { data: { items, total: data.total } };
}
return { error };
}
}

View File

@@ -114,6 +114,7 @@
"@umbraco-cms/backoffice/external/dompurify": ["./src/external/dompurify/index.ts"],
"@umbraco-cms/backoffice/external/lit": ["./src/external/lit/index.ts"],
"@umbraco-cms/backoffice/external/marked": ["./src/external/marked/index.ts"],
"@umbraco-cms/backoffice/external/mime-types": ["./src/external/mime-types/index.ts"],
"@umbraco-cms/backoffice/external/monaco-editor": ["./src/external/monaco-editor/index.ts"],
"@umbraco-cms/backoffice/external/openid": ["./src/external/openid/index.ts"],
"@umbraco-cms/backoffice/external/router-slot": ["./src/external/router-slot/index.ts"],