Skip to content

Commit 937f4b8

Browse files
authored
V16: Media Picker property editor does not handle dropped files appropriately (#19900)
* fix: make it clear that the clearUploads button is used to "Clear file(s)" and not necessarily remove them (from the dropzone) * fix: adds extra null-check to avoid browser error on failed uploads * fix: adds check that no media files are added twice (or more) to the media picker * fix: adds try/catch around confirm modal to avoid browser error in case user cancels * fix: change from deprecated 'complete' event to 'change' event and filter out non-successful files * chore: sort imports * feat: renders the 'add' button even if the limits have been exceeded * feat: shows all values as cards even if the media item does not exist so the user has a chance to update the value * feat: shows all values as cards even if the media item does not exist so the user has a chance to update the value * feat: adds localization to the media picker context * feat: uses the media picker context to control the picker this also fixes an issue where already selected items were not preselected when opening the picker again * feat: adds a bit of margin between the dropzone and media picker itself
1 parent cee441d commit 937f4b8

File tree

9 files changed

+92
-79
lines changed

9 files changed

+92
-79
lines changed

src/Umbraco.Web.UI.Client/src/assets/lang/da.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export default {
293293
notCreated: 'Ikke oprettet',
294294
updateDate: 'Sidst redigeret',
295295
updateDateDesc: 'Tidspunkt for seneste redigering',
296-
uploadClear: 'Fjern fil',
296+
uploadClear: 'Fjern fil(er)',
297297
uploadClearImageContext: 'Klik her for at fjerne billedet fra medie filen',
298298
uploadClearFileContext: 'Klik her for at fjerne filen fra medie filen',
299299
urls: 'Link til dokument',

src/Umbraco.Web.UI.Client/src/assets/lang/de.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export default {
277277
notCreated: 'Nicht angelegt',
278278
updateDate: 'Zuletzt bearbeitet am',
279279
updateDateDesc: 'Letzter Änderungszeitpunkt des Dokuments',
280-
uploadClear: 'Datei entfernen',
280+
uploadClear: 'Datei(en) entfernen',
281281
uploadClearImageContext: 'Klicke hier um das das Bild vom Medienelement zu entfernen.',
282282
uploadClearFileContext: 'Klicke hier um das das Bild vom Medienelement zu entfernen.',
283283
urls: 'Link zum Dokument',

src/Umbraco.Web.UI.Client/src/assets/lang/en.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export default {
176176
confirmActionConfirm: 'Confirm',
177177
morePublishingOptions: 'More publishing options',
178178
submitChanges: 'Submit',
179-
viewSystemDetails:"View Umbraco CMS system information and version number"
179+
viewSystemDetails: 'View Umbraco CMS system information and version number',
180180
},
181181
auditTrailsMedia: {
182182
delete: 'Media deleted',
@@ -295,7 +295,7 @@ export default {
295295
notCreated: 'Not created',
296296
updateDate: 'Last edited',
297297
updateDateDesc: 'Date/time this document was edited',
298-
uploadClear: 'Remove file(s)',
298+
uploadClear: 'Clear file(s)',
299299
uploadClearImageContext: 'Click here to remove the image from the media item',
300300
uploadClearFileContext: 'Click here to remove the file from the media item',
301301
urls: 'Link to document',

src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ export class UmbPickerInputContext<
112112

113113
await umbConfirmModal(this, {
114114
color: 'danger',
115-
headline: `Remove ${item.name}?`,
116-
content: 'Are you sure you want to remove this item',
115+
headline: `#actions_remove ${item.name}?`,
116+
content: `#defaultdialogs_confirmremove ${item.name}?`,
117117
confirmLabel: '#actions_remove',
118118
});
119119

src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin<UmbUploadableIt
170170
compact
171171
@click=${this.#handleRemove}
172172
label=${this.localize.term('content_uploadClear')}>
173-
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
173+
<uui-icon name="icon-trash"></uui-icon>
174+
<umb-localize key="content_uploadClear">Clear file(s)</umb-localize>
174175
</uui-button>
175176
</div>
176177
`;

src/Umbraco.Web.UI.Client/src/packages/media/imaging/imaging.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {
5151
continue;
5252
}
5353

54-
const url = urlModels?.[0].url;
54+
const url = urlModels?.[0]?.url;
5555

5656
this.#dataStore.addCrop(unique, url ?? '', imagingModel);
5757

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
177177
<umb-image-cropper-field .value=${this.value} .file=${this._file?.temporaryFile?.file} @change=${this.#onChange}>
178178
<uui-button slot="actions" compact label=${this.localize.term('content_uploadClear')} @click=${this.#onRemove}>
179179
<uui-icon name="icon-trash"></uui-icon>
180-
<umb-localize key="content_uploadClear">Remove file(s)</umb-localize>
180+
<umb-localize key="content_uploadClear">Clear file(s)</umb-localize>
181181
</uui-button>
182182
</umb-image-cropper-field>
183183
`;

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts

Lines changed: 80 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js';
1+
import { UMB_IMAGE_CROPPER_EDITOR_MODAL } from '../../modals/index.js';
22
import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js';
33
import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/constants.js';
4-
import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone';
5-
import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
6-
import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
4+
import { UmbMediaPickerInputContext } from '../input-media/input-media.context.js';
5+
import { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone';
6+
import type { UmbDropzoneChangeEvent } from '@umbraco-cms/backoffice/dropzone';
7+
import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
78
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
89
import { UmbId } from '@umbraco-cms/backoffice/id';
910
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -14,6 +15,7 @@ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
1415
import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree';
1516
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
1617
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
18+
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/media-type';
1719

1820
import '@umbraco-cms/backoffice/imaging';
1921

@@ -24,6 +26,7 @@ type UmbRichMediaCardModel = {
2426
src?: string;
2527
icon?: string;
2628
isTrashed?: boolean;
29+
isLoading?: boolean;
2730
};
2831

2932
@customElement('umb-input-rich-media')
@@ -99,6 +102,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
99102
public override set value(value: Array<UmbMediaPickerPropertyValueEntry> | undefined) {
100103
super.value = value;
101104
this.#sorter.setModel(value);
105+
this.#pickerContext.setSelection(value?.map((item) => item.mediaKey) ?? []);
102106
this.#itemManager.setUniques(value?.map((x) => x.mediaKey));
103107
// Maybe the new value is using an existing media, and there we need to update the cards despite no repository update.
104108
this.#populateCards();
@@ -175,6 +179,8 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
175179

176180
readonly #itemManager = new UmbRepositoryItemsManager<UmbMediaItemModel>(this, UMB_MEDIA_ITEM_REPOSITORY_ALIAS);
177181

182+
readonly #pickerContext = new UmbMediaPickerInputContext(this);
183+
178184
constructor() {
179185
super();
180186

@@ -228,6 +234,10 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
228234
this._routeBuilder = routeBuilder;
229235
});
230236

237+
this.observe(this.#pickerContext.selection, (selection) => {
238+
this.#addItems(selection);
239+
});
240+
231241
this.addValidator(
232242
'valueMissing',
233243
() => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
@@ -262,16 +272,6 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
262272
async #populateCards() {
263273
const mediaItems = this.#itemManager.getItems();
264274

265-
if (!mediaItems.length) {
266-
this._cards = [];
267-
return;
268-
}
269-
// Check if all media items is loaded.
270-
// But notice, it would be nicer UX if we could show a loading state on the cards that are missing(loading) their items.
271-
const missingCards = mediaItems.filter((item) => !this._cards.find((card) => card.unique === item.unique));
272-
const removedCards = this._cards.filter((card) => !mediaItems.find((item) => card.unique === item.unique));
273-
if (missingCards.length === 0 && removedCards.length === 0) return;
274-
275275
this._cards =
276276
this.value?.map((item) => {
277277
const media = mediaItems.find((x) => x.unique === item.mediaKey);
@@ -281,6 +281,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
281281
name: media?.name ?? '',
282282
icon: media?.mediaType?.icon,
283283
isTrashed: media?.isTrashed ?? false,
284+
isLoading: !media,
284285
};
285286
}) ?? [];
286287
}
@@ -292,7 +293,10 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
292293
return true;
293294
};
294295

295-
#addItems(uniques: string[]) {
296+
#addItems(additionalMediaKeys: string[]) {
297+
// Check that the unique is not already added
298+
const uniques = additionalMediaKeys.filter((key) => !this.value?.some((item) => item.mediaKey === key));
299+
296300
if (!uniques.length) return;
297301

298302
const additions: Array<UmbMediaPickerPropertyValueEntry> = uniques.map((unique) => ({
@@ -307,40 +311,40 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
307311
this.dispatchEvent(new UmbChangeEvent());
308312
}
309313

310-
async #openPicker() {
311-
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
312-
const modalHandler = modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, {
313-
data: {
314+
#openPicker() {
315+
this.#pickerContext.openPicker(
316+
{
314317
multiple: this.multiple,
315318
startNode: this.startNode,
316319
pickableFilter: this.#pickableFilter,
317320
},
318-
value: { selection: [] },
319-
});
320-
321-
const data = await modalHandler?.onSubmit().catch(() => null);
322-
if (!data) return;
323-
324-
const selection = data.selection.filter((x) => x !== null) as string[];
325-
this.#addItems(selection);
321+
{
322+
allowedContentTypes: this.allowedContentTypeIds?.map((id) => ({
323+
unique: id,
324+
entityType: UMB_MEDIA_TYPE_ENTITY_TYPE,
325+
})),
326+
includeTrashed: false,
327+
},
328+
);
326329
}
327330

328331
async #onRemove(item: UmbRichMediaCardModel) {
329-
await umbConfirmModal(this, {
330-
color: 'danger',
331-
headline: `${this.localize.term('actions_remove')} ${item.name}?`,
332-
content: `${this.localize.term('defaultdialogs_confirmremove')} ${item.name}?`,
333-
confirmLabel: this.localize.term('actions_remove'),
334-
});
335-
336-
this.value = this.value?.filter((x) => x.key !== item.unique);
337-
338-
this.dispatchEvent(new UmbChangeEvent());
332+
try {
333+
await this.#pickerContext.requestRemoveItem(item.media);
334+
this.value = this.value?.filter((x) => x.key !== item.unique);
335+
this.dispatchEvent(new UmbChangeEvent());
336+
} catch {
337+
// User cancelled the action
338+
}
339339
}
340340

341-
async #onUploadCompleted(e: CustomEvent) {
342-
const completed = e.detail as Array<UmbUploadableItem>;
343-
const uploaded = completed.map((file) => file.unique);
341+
async #onUploadCompleted(e: UmbDropzoneChangeEvent) {
342+
if (this.readonly) return;
343+
344+
// If there are any finished uploadable items, we need to add them to the value
345+
const uploaded = e.items
346+
.filter((file) => file.status === UmbFileDropzoneItemStatus.COMPLETE)
347+
.map((file) => file.unique);
344348
this.#addItems(uploaded);
345349
}
346350

@@ -351,16 +355,17 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
351355
`;
352356
}
353357

358+
// TODO: Consider removing the "progress element" from the dropzone and render that using a context instead. This would allow the media picker to show inline progress items instead [JOV]
354359
#renderDropzone() {
355360
if (this.readonly) return nothing;
356-
if (this._cards && this._cards.length >= this.max) return;
357361
return html`<umb-dropzone-media
358-
?multiple=${this.max > 1}
359-
@complete=${this.#onUploadCompleted}></umb-dropzone-media>`;
362+
id="dropzone"
363+
?multiple=${this.multiple}
364+
@change=${this.#onUploadCompleted}></umb-dropzone-media>`;
360365
}
361366

362367
#renderItems() {
363-
if (!this._cards.length) return;
368+
if (!this._cards.length) return nothing;
364369
return html`
365370
${repeat(
366371
this._cards,
@@ -371,37 +376,39 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
371376
}
372377

373378
#renderAddButton() {
374-
if (this._cards && this._cards.length && !this.multiple) return;
375-
if (this.readonly && this._cards.length > 0) {
376-
return nothing;
377-
} else {
378-
return html`
379-
<uui-button
380-
id="btn-add"
381-
look="placeholder"
382-
@blur=${() => {
383-
this.pristine = false;
384-
this.checkValidity();
385-
}}
386-
@click=${this.#openPicker}
387-
label=${this.localize.term('general_choose')}
388-
?disabled=${this.readonly}>
389-
<uui-icon name="icon-add"></uui-icon>
390-
${this.localize.term('general_choose')}
391-
</uui-button>
392-
`;
393-
}
379+
if (this.readonly) return nothing;
380+
return html`
381+
<uui-button
382+
id="btn-add"
383+
look="placeholder"
384+
@blur=${() => {
385+
this.pristine = false;
386+
this.checkValidity();
387+
}}
388+
@click=${this.#openPicker}
389+
label=${this.localize.term('general_choose')}
390+
?disabled=${this.readonly}>
391+
<uui-icon name="icon-add"></uui-icon>
392+
${this.localize.term('general_choose')}
393+
</uui-button>
394+
`;
394395
}
395396

396397
#renderItem(item: UmbRichMediaCardModel) {
397398
if (!item.unique) return nothing;
398399
const href = this.readonly ? undefined : this._routeBuilder?.({ key: item.unique });
399400
return html`
400401
<uui-card-media id=${item.unique} name=${item.name} .href=${href} ?readonly=${this.readonly}>
401-
<umb-imaging-thumbnail
402-
unique=${item.media}
403-
alt=${item.name}
404-
icon=${item.icon ?? 'icon-picture'}></umb-imaging-thumbnail>
402+
${when(
403+
item.isLoading,
404+
() => html`<uui-loader-circle></uui-loader-circle>`,
405+
() => html`
406+
<umb-imaging-thumbnail
407+
unique=${item.media}
408+
alt=${item.name}
409+
icon=${item.icon ?? 'icon-picture'}></umb-imaging-thumbnail>
410+
`,
411+
)}
405412
${this.#renderIsTrashed(item)} ${this.#renderActions(item)}
406413
</uui-card-media>
407414
`;
@@ -427,7 +434,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
427434
`;
428435
}
429436

430-
static override styles = [
437+
static override readonly styles = [
431438
css`
432439
:host {
433440
position: relative;
@@ -439,6 +446,10 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin<
439446
grid-auto-rows: var(--umb-card-medium-min-width);
440447
}
441448
449+
#dropzone {
450+
margin-bottom: var(--uui-size-space-5);
451+
}
452+
442453
#btn-add {
443454
text-align: center;
444455
height: 100%;

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
201201
#renderButtonRemove() {
202202
return html`
203203
<uui-button compact @click=${this.#handleRemove} label=${this.localize.term('content_uploadClear')}>
204-
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
204+
<uui-icon name="icon-trash"></uui-icon>
205+
<umb-localize key="content_uploadClear">Clear file(s)</umb-localize>
205206
</uui-button>
206207
`;
207208
}

0 commit comments

Comments
 (0)