Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media dropzone improvements (Batch upload, Upload sorting, Folder upload, Progress notification) #2258

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/lang/da-dk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ export default {
uploadNotAllowed: 'Upload er ikke tiladt på denne lokation',
disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here",
invalidFileName: 'Cannot upload this file, it does not have a valid file name',
selectMediaTypeForUpload: 'Vælg medie type for uploadet filer.',
selectMediaTypeForUploadHelp: 'Se påvirket filer.',
selectMediaTypeAutoPick: 'Automatisk',
},
member: {
createNewMember: 'Opret et nyt medlem',
Expand Down
3 changes: 3 additions & 0 deletions src/assets/lang/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ export default {
renameFolderFailed: 'Failed to rename the folder with id %0%',
dragAndDropYourFilesIntoTheArea: 'Drag and drop your file(s) into the area',
uploadNotAllowed: 'Upload is not allowed in this location.',
selectMediaTypeForUpload: 'Select media type for uploaded files.',
selectMediaTypeForUploadHelp: 'See files affected files.',
selectMediaTypeAutoPick: 'Auto pick',
},
member: {
createNewMember: 'Create a new member',
Expand Down
3 changes: 3 additions & 0 deletions src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ export default {
fileSecurityValidationFailure: 'One or more file security validations have failed',
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
uploadNotAllowed: 'Upload is not allowed in this location.',
selectMediaTypeForUpload: 'Select media type for uploaded files.',
selectMediaTypeForUploadHelp: 'See files affected files.',
selectMediaTypeAutoPick: 'Auto pick',
},
member: {
createNewMember: 'Create a new member',
Expand Down
25 changes: 23 additions & 2 deletions src/packages/core/notification/notification-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export class UmbNotificationHandler {
*/
constructor(options: UmbNotificationOptions) {
this.key = UmbId.new();
this.color = options.color || this._defaultColor;
this.color = options.color ?? this._defaultColor;
this.duration = options.duration !== undefined ? options.duration : this._defaultDuration;

this._elementName = options.elementName || this._defaultLayout;
this._elementName = options.elementName ?? this._defaultLayout;
this._data = options.data;

this._closePromise = new Promise((res) => {
Expand Down Expand Up @@ -65,6 +65,27 @@ export class UmbNotificationHandler {
this.element = notification;
}

/**
* Updates the notification
* @param {UmbNotificationOptions} options - The new options
*/
public Update(options: UmbNotificationOptions) {
this.color = options.color ?? this.color;
this.duration = options.duration !== undefined ? options.duration : this.duration;

this._elementName = options.elementName ?? this._elementName;

this._data = options.data;

this.element.color = this.color;
this.element.autoClose = this.duration;

const notificationChild = this.element.children?.[0];
if (notificationChild?.data) {
notificationChild.data = { ...this.element.children[0].data, ...this._data };
}
}

/**
* @param {...any} args
* @memberof UmbNotificationHandler
Expand Down
12 changes: 12 additions & 0 deletions src/packages/core/notification/notification.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ export class UmbNotificationContext extends UmbContextBase<UmbNotificationContex
public stay(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler {
return this._open({ ...options, color, duration: null });
}

/**
* Updates the notification
* @param {UmbNotificationHandler} handler The handler of the notification
* @param {UmbNotificationOptions} options The new options
*/
public update(handler: UmbNotificationHandler, options: UmbNotificationOptions) {
const notification = this._notifications.getValue().find((notification) => notification.key === handler.key);
if (notification) {
notification.Update(options);
}
}
}

export const UMB_NOTIFICATION_CONTEXT = new UmbContextToken<UmbNotificationContext>('UmbNotificationContext');
4 changes: 2 additions & 2 deletions src/packages/core/repository/detail/detail-repository-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export abstract class UmbDetailRepositoryBase<
this.#detailStore?.append(createdData);

// TODO: how do we handle generic notifications? Is this the correct place to do it?
const notification = { data: { message: `Created` } };
this.#notificationContext!.peek('positive', notification);
// const notification = { data: { message: `Created` } };
// this.#notificationContext!.peek('positive', notification);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like some sparing on what HQ finds would be the best way to handle this. I've disabled the notification here because when uploading many files the right hand side of the screen just becomes a green wall of Created messages. I think the best way for the drag and drop feature is to not have any Created messages at all and only the progress and failure notification messages show up but is it feasible to move the notification from here closer to the places we would like to show it I can see this method has many references.

}

return { data: createdData, error };
Expand Down
62 changes: 42 additions & 20 deletions src/packages/core/temporary-file/temporary-file-manager.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';

///export type TemporaryFileStatus = 'success' | 'waiting' | 'error';

export enum TemporaryFileStatus {
SUCCESS = 'success',
WAITING = 'waiting',
Expand All @@ -17,6 +15,13 @@ export interface UmbTemporaryFileModel {
status?: TemporaryFileStatus;
}

type QueueHandlerCallback<TItem extends UmbTemporaryFileModel> = (item: TItem) => Promise<void>;

type UploadOptions<TItem extends UmbTemporaryFileModel> = {
chunkSize?: number;
callback?: QueueHandlerCallback<TItem>;
};

export class UmbTemporaryFileManager<
UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel,
> extends UmbControllerBase {
Expand All @@ -30,7 +35,7 @@ export class UmbTemporaryFileManager<
this.#temporaryFileRepository = new UmbTemporaryFileRepository(host);
}

async uploadOne(uploadableItem: UploadableItem): Promise<UploadableItem> {
async uploadOne(uploadableItem: UploadableItem, options?: UploadOptions<UploadableItem>): Promise<UploadableItem> {
this.#queue.setValue([]);

const item: UploadableItem = {
Expand All @@ -39,15 +44,18 @@ export class UmbTemporaryFileManager<
};

this.#queue.appendOne(item);
return (await this.#handleQueue())[0];
return (await this.#handleQueue({ ...options, chunkSize: 1 }))[0];
}

async upload(queueItems: Array<UploadableItem>): Promise<Array<UploadableItem>> {
async upload(
queueItems: Array<UploadableItem>,
options?: UploadOptions<UploadableItem>,
): Promise<Array<UploadableItem>> {
this.#queue.setValue([]);

const items = queueItems.map((item): UploadableItem => ({ status: TemporaryFileStatus.WAITING, ...item }));
this.#queue.append(items);
return this.#handleQueue();
return this.#handleQueue(options);
}

removeOne(unique: string) {
Expand All @@ -58,30 +66,44 @@ export class UmbTemporaryFileManager<
this.#queue.remove(uniques);
}

async #handleQueue() {
async #handleQueue(options?: UploadOptions<UploadableItem>): Promise<Array<UploadableItem>> {
const filesCompleted: Array<UploadableItem> = [];
const queue = this.#queue.getValue();
const chunkSize = options?.chunkSize ?? 5;

if (!queue.length) return filesCompleted;

for (const item of queue) {
if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`);
const chunks = Math.ceil(queue.length / chunkSize);

const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file);
//await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
const handler: QueueHandlerCallback<UploadableItem> = async (item) => {
const completedUpload = await this.#handleUpload(item);
filesCompleted.push(completedUpload);

let status: TemporaryFileStatus;
if (error) {
status = TemporaryFileStatus.ERROR;
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
} else {
status = TemporaryFileStatus.SUCCESS;
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
}
if (options?.callback) await options.callback(completedUpload);
};

filesCompleted.push({ ...item, status });
for (let i = 0; i < chunks; i++) {
const chunk = queue.slice(i * chunkSize, i * chunkSize + chunkSize);
await Promise.all(chunk.map(handler));
}

return filesCompleted;
}

async #handleUpload(item: UploadableItem) {
if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`);

const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file);

let status: TemporaryFileStatus;
if (error) {
status = TemporaryFileStatus.ERROR;
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
} else {
status = TemporaryFileStatus.SUCCESS;
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
}

return { ...item, status };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement<

#onFileDropped() {
const data = this.dropzone?.getFiles()[0];
if (!data) return;
if (!data || !('temporaryUnique' in data)) return;

this.#temporaryUnique = data.temporaryUnique;
this.#fileReader.readAsText(data.file);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<

#onFileDropped() {
const data = this.dropzone?.getFiles()[0];
if (!data) return;
if (!data || !('temporaryUnique' in data)) return;

this.#temporaryUnique = data.temporaryUnique;
this.#fileReader.readAsText(data.file);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepo
}) {
return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take });
}

async requestFolderMediaTypes({ skip = 0, take = 100 }: { skip?: number; take?: number }) {
return this.#dataSource.getMediaTypesFolders({ skip, take });
}
}

export default UmbMediaTypeStructureRepository;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu
getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) {
return getAllowedMediaTypesOfExtension({ fileExtension, skip, take });
}

getMediaTypesFolders({ skip, take }: { skip: number; take: number }) {
return getItemMediaTypeFolders({ skip, take });
}
}

const getAllowedChildrenOf = (unique: string | null) => {
Expand Down Expand Up @@ -55,3 +59,9 @@ const getAllowedMediaTypesOfExtension = async ({
const { items } = await MediaTypeService.getItemMediaTypeAllowed({ fileExtension, skip, take });
return items.map((item) => mapper(item));
};

const getItemMediaTypeFolders = 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));
};
22 changes: 20 additions & 2 deletions src/packages/media/media/collection/media-collection.element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/e
@customElement('umb-media-collection')
export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
#mediaCollection?: UmbMediaCollectionContext;
#refreshInCooldown: boolean = false;
#shouldRefreshCollection: boolean = false;

@state()
private _progress = -1;
Expand All @@ -33,8 +35,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
}

async #onChange() {
this._progress = -1;
this.#mediaCollection?.requestCollection();
this.#refreshCollection();

const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadChildrenOfEntityEvent({
Expand All @@ -48,12 +49,29 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
this._progress = event.progress;
}

#refreshCollection() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid over updating the UI this has been added to we don't update the UI more than every 350ms

if (!this.#refreshInCooldown) {
this.#mediaCollection?.requestCollection();
this.#refreshInCooldown = true;
setTimeout(() => {
this.#refreshInCooldown = false;
if (this.#shouldRefreshCollection) {
this.#refreshCollection();
this.#shouldRefreshCollection = false;
}
}, 350);
} else {
this.#shouldRefreshCollection = true;
}
}

protected override renderToolbar() {
return html`
<umb-media-collection-toolbar slot="header"></umb-media-collection-toolbar>
${when(this._progress >= 0, () => html`<uui-loader-bar progress=${this._progress}></uui-loader-bar>`)}
<umb-dropzone
.parentUnique=${this._unique}
multiple
@change=${this.#onChange}
@progress=${this.#onProgress}></umb-dropzone>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`<umb-dropzone @change=${this.#onUploadCompleted}></umb-dropzone>`;
return html`<umb-dropzone multiple @change=${this.#onUploadCompleted}></umb-dropzone>`;
}

#renderItems() {
Expand Down
Loading
Loading