Merge branch 'main' into bugfix/reload-collection-on-entity-structure-reload-request
This commit is contained in:
@@ -25,7 +25,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
|
||||
this._src = value.src;
|
||||
}
|
||||
get value(): MediaValueType {
|
||||
return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique };
|
||||
return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.temporaryUnique };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +67,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
|
||||
async #onUpload(e: UUIFileDropzoneEvent) {
|
||||
//Property Editor for Upload field will always only have one file.
|
||||
const item: UmbTemporaryFileModel = {
|
||||
unique: UmbId.new(),
|
||||
temporaryUnique: UmbId.new(),
|
||||
file: e.detail.files[0],
|
||||
};
|
||||
const upload = this.#manager.uploadOne(item);
|
||||
@@ -80,7 +80,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
|
||||
|
||||
const uploaded = await upload;
|
||||
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
|
||||
this.temporaryFile = { unique: item.unique, file: item.file };
|
||||
this.temporaryFile = { temporaryUnique: item.temporaryUnique, file: item.file };
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,9 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
uui-icon {
|
||||
vertical-align: sub;
|
||||
margin-right: var(--uui-size-space-4);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js';
|
||||
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';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
|
||||
///export type TemporaryFileStatus = 'success' | 'waiting' | 'error';
|
||||
|
||||
@@ -14,7 +13,7 @@ export enum TemporaryFileStatus {
|
||||
|
||||
export interface UmbTemporaryFileModel {
|
||||
file: File;
|
||||
unique: string;
|
||||
temporaryUnique: string;
|
||||
status?: TemporaryFileStatus;
|
||||
}
|
||||
|
||||
@@ -23,7 +22,7 @@ export class UmbTemporaryFileManager<
|
||||
> extends UmbControllerBase {
|
||||
#temporaryFileRepository;
|
||||
|
||||
#queue = new UmbArrayState<UploadableItem>([], (item) => item.unique);
|
||||
#queue = new UmbArrayState<UploadableItem>([], (item) => item.temporaryUnique);
|
||||
public readonly queue = this.#queue.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
@@ -66,18 +65,18 @@ export class UmbTemporaryFileManager<
|
||||
if (!queue.length) return filesCompleted;
|
||||
|
||||
for (const item of queue) {
|
||||
if (!item.unique) throw new Error(`Unique is missing for item ${item}`);
|
||||
if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`);
|
||||
|
||||
const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file);
|
||||
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
|
||||
|
||||
let status: TemporaryFileStatus;
|
||||
if (error) {
|
||||
status = TemporaryFileStatus.ERROR;
|
||||
this.#queue.updateOne(item.unique, { ...item, status });
|
||||
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
|
||||
} else {
|
||||
status = TemporaryFileStatus.SUCCESS;
|
||||
this.#queue.updateOne(item.unique, { ...item, status });
|
||||
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
|
||||
}
|
||||
|
||||
filesCompleted.push({ ...item, status });
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
|
||||
import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js';
|
||||
import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js';
|
||||
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
|
||||
UmbMediaCollectionItemModel,
|
||||
@@ -21,7 +22,10 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
|
||||
this.observe(this.items, async (items) => {
|
||||
if (!items?.length) return;
|
||||
|
||||
const { data } = await this.#imagingRepository.requestResizedItems(items.map((m) => m.unique));
|
||||
const { data } = await this.#imagingRepository.requestResizedItems(
|
||||
items.map((m) => m.unique),
|
||||
{ height: 400, width: 400, mode: ImageCropModeModel.MIN },
|
||||
);
|
||||
|
||||
this.#thumbnailItems.setValue(
|
||||
items.map((item) => {
|
||||
|
||||
@@ -116,7 +116,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderItem(item: UmbMediaCollectionItemModel) {
|
||||
// TODO: Fix the file extension when media items have a file extension. [?]
|
||||
return html`
|
||||
<uui-card-media
|
||||
.name=${item.name ?? 'Unnamed Media'}
|
||||
@@ -126,8 +125,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
|
||||
@open=${(event: Event) => this.#onOpen(event, item.unique)}
|
||||
@selected=${() => this.#onSelect(item)}
|
||||
@deselected=${() => this.#onDeselect(item)}
|
||||
class="media-item"
|
||||
file-ext="${item.icon}">
|
||||
class="media-item">
|
||||
${item.url ? html`<img src=${item.url} alt=${item.name} />` : html`<umb-icon name=${item.icon}></umb-icon>`}
|
||||
<!-- TODO: [LK] I'd like to indicate a busy state when bulk actions are triggered. -->
|
||||
<!-- <div class="container"><uui-loader></uui-loader></div> -->
|
||||
@@ -156,7 +154,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
|
||||
gap: var(--uui-size-space-5);
|
||||
}
|
||||
umb-icon {
|
||||
font-size: var(--uui-size-24);
|
||||
font-size: var(--uui-size-8);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -5,20 +5,15 @@ import { LitElement, css, html, nothing, customElement, property, query } from '
|
||||
|
||||
@customElement('umb-image-cropper-focus-setter')
|
||||
export class UmbImageCropperFocusSetterElement extends LitElement {
|
||||
@query('#image') imageElement?: HTMLImageElement;
|
||||
@query('#image') imageElement!: HTMLImageElement;
|
||||
@query('#wrapper') wrapperElement?: HTMLImageElement;
|
||||
@query('#focal-point') focalPointElement?: HTMLImageElement;
|
||||
@query('#focal-point') focalPointElement!: HTMLImageElement;
|
||||
|
||||
@property({ type: String }) src?: string;
|
||||
@property({ attribute: false }) focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 };
|
||||
|
||||
#DOT_RADIUS = 6 as const;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#removeEventListeners();
|
||||
@@ -33,33 +28,46 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.update(changedProperties);
|
||||
|
||||
if (changedProperties.has('src')) {
|
||||
if (this.src) {
|
||||
this.#initializeImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this.style.setProperty('--dot-radius', `${this.#DOT_RADIUS}px`);
|
||||
}
|
||||
|
||||
if (this.focalPointElement) {
|
||||
this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
}
|
||||
if (this.imageElement) {
|
||||
this.imageElement.onload = () => {
|
||||
if (!this.imageElement || !this.wrapperElement) return;
|
||||
const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight;
|
||||
const hostRect = this.getBoundingClientRect();
|
||||
const image = this.imageElement.getBoundingClientRect();
|
||||
async #initializeImage() {
|
||||
await this.updateComplete; // Wait for the @query to be resolved
|
||||
|
||||
if (image.width > hostRect.width) {
|
||||
this.imageElement.style.width = '100%';
|
||||
}
|
||||
if (image.height > hostRect.height) {
|
||||
this.imageElement.style.height = '100%';
|
||||
}
|
||||
this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
|
||||
this.imageElement.style.aspectRatio = `${imageAspectRatio}`;
|
||||
this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`;
|
||||
};
|
||||
}
|
||||
this.imageElement.onload = () => {
|
||||
if (!this.imageElement || !this.wrapperElement) return;
|
||||
const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight;
|
||||
const hostRect = this.getBoundingClientRect();
|
||||
const image = this.imageElement.getBoundingClientRect();
|
||||
|
||||
if (image.width > hostRect.width) {
|
||||
this.imageElement.style.width = '100%';
|
||||
}
|
||||
if (image.height > hostRect.height) {
|
||||
this.imageElement.style.height = '100%';
|
||||
}
|
||||
|
||||
this.imageElement.style.aspectRatio = `${imageAspectRatio}`;
|
||||
this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`;
|
||||
};
|
||||
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
async #addEventListeners() {
|
||||
@@ -134,6 +142,7 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
|
||||
}
|
||||
/* Wrapper is used to make the focal point position responsive to the image size */
|
||||
#wrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: auto;
|
||||
|
||||
@@ -33,11 +33,7 @@ export class UmbImageCropperPreviewElement extends LitElement {
|
||||
if (!this.crop) return;
|
||||
|
||||
await this.updateComplete; // Wait for the @query to be resolved
|
||||
|
||||
if (!this.imageElement.complete) {
|
||||
// Wait for the image to load
|
||||
await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement)));
|
||||
}
|
||||
await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement)));
|
||||
|
||||
const container = this.imageContainerElement.getBoundingClientRect();
|
||||
const cropAspectRatio = this.crop.width / this.crop.height;
|
||||
|
||||
@@ -376,6 +376,7 @@ export class UmbImageCropperElement extends LitElement {
|
||||
#image {
|
||||
display: block;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#slider {
|
||||
|
||||
@@ -56,7 +56,7 @@ export class UmbInputImageCropperElement extends UmbLitElement {
|
||||
|
||||
this.value = assignToFrozenObject(this.value, { temporaryFileId: unique });
|
||||
|
||||
this.#manager?.uploadOne({ unique, file });
|
||||
this.#manager?.uploadOne({ temporaryUnique: unique, file });
|
||||
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
@@ -68,8 +68,9 @@ export class UmbInputImageCropperElement extends UmbLitElement {
|
||||
|
||||
#onRemove = () => {
|
||||
this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null });
|
||||
if (!this.fileUnique) return;
|
||||
this.#manager?.removeOne(this.fileUnique);
|
||||
if (this.fileUnique) {
|
||||
this.#manager?.removeOne(this.fileUnique);
|
||||
}
|
||||
this.fileUnique = undefined;
|
||||
this.file = undefined;
|
||||
|
||||
@@ -114,7 +115,7 @@ export class UmbInputImageCropperElement extends UmbLitElement {
|
||||
const value = (e.target as UmbInputImageCropperFieldElement).value;
|
||||
|
||||
if (!value) {
|
||||
this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 } };
|
||||
this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, temporaryFileId: null };
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { UMB_MEDIA_PICKER_MODAL, type UmbMediaCardItemModel } from '../../modals/index.js';
|
||||
import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js';
|
||||
import type { UmbMediaItemModel } from '../../repository/item/types.js';
|
||||
import type { UmbMediaTreeItemModel } from '../../tree/index.js';
|
||||
import { UMB_MEDIA_TREE_PICKER_MODAL } from '../../tree/index.js';
|
||||
import type {
|
||||
UmbMediaTreePickerModalData,
|
||||
UmbMediaTreePickerModalValue,
|
||||
} from '../../tree/media-tree-picker-modal.token.js';
|
||||
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
|
||||
import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export class UmbMediaPickerContext extends UmbPickerInputContext<
|
||||
UmbMediaItemModel,
|
||||
@@ -15,7 +18,38 @@ export class UmbMediaPickerContext extends UmbPickerInputContext<
|
||||
UmbMediaTreePickerModalData,
|
||||
UmbMediaTreePickerModalValue
|
||||
> {
|
||||
#imagingRepository: UmbImagingRepository;
|
||||
|
||||
#cardItems = new UmbArrayState<UmbMediaCardItemModel>([], (x) => x.unique);
|
||||
readonly cardItems = this.#cardItems.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL);
|
||||
super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_PICKER_MODAL);
|
||||
this.#imagingRepository = new UmbImagingRepository(host);
|
||||
|
||||
this.observe(this.selectedItems, async (selectedItems) => {
|
||||
if (!selectedItems?.length) {
|
||||
this.#cardItems.setValue([]);
|
||||
return;
|
||||
}
|
||||
const { data } = await this.#imagingRepository.requestResizedItems(
|
||||
selectedItems.map((x) => x.unique),
|
||||
{ height: 400, width: 400, mode: ImageCropModeModel.MIN },
|
||||
);
|
||||
|
||||
this.#cardItems.setValue(
|
||||
selectedItems.map((item) => {
|
||||
const url = data?.find((x) => x.unique === item.unique)?.url;
|
||||
return {
|
||||
icon: item.mediaType.icon,
|
||||
name: item.name,
|
||||
unique: item.unique,
|
||||
isTrashed: item.isTrashed,
|
||||
entityType: item.entityType,
|
||||
url,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UmbMediaCardItemModel } from '../../modals/index.js';
|
||||
import type { UmbMediaItemModel } from '../../repository/index.js';
|
||||
import { UmbMediaPickerContext } from './input-media.context.js';
|
||||
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -7,6 +8,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbModalRouteRegistrationController, UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
|
||||
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { UmbUploadableFileModel } from '@umbraco-cms/backoffice/media';
|
||||
|
||||
@customElement('umb-input-media')
|
||||
export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') {
|
||||
@@ -100,7 +102,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
private _editMediaPath = '';
|
||||
|
||||
@state()
|
||||
private _items?: Array<UmbMediaItemModel>;
|
||||
private _items?: Array<UmbMediaCardItemModel>;
|
||||
|
||||
#pickerContext = new UmbMediaPickerContext(this);
|
||||
|
||||
@@ -117,7 +119,9 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
});
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
this.observe(this.#pickerContext.cardItems, (cardItems) => {
|
||||
this._items = cardItems;
|
||||
});
|
||||
|
||||
this.addValidator(
|
||||
'rangeUnderflow',
|
||||
@@ -150,12 +154,29 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
});
|
||||
}
|
||||
|
||||
async #onUploadCompleted(e: CustomEvent) {
|
||||
const completed = e.detail?.completed as Array<UmbUploadableFileModel>;
|
||||
const uploaded = completed.map((file) => file.unique);
|
||||
|
||||
this.selection = [...this.selection, ...uploaded];
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div class="container">${this.#renderItems()} ${this.#renderAddButton()}</div>`;
|
||||
return html`${this.#renderDropzone()}
|
||||
<div class="container">${this.#renderItems()} ${this.#renderAddButton()}</div>`;
|
||||
}
|
||||
|
||||
#renderDropzone() {
|
||||
if (this._items && this._items.length >= this.max) return;
|
||||
return html`<umb-dropzone
|
||||
id="dropzone"
|
||||
?multiple=${this.max === 1}
|
||||
@change=${this.#onUploadCompleted}></umb-dropzone>`;
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
if (!this._items) return;
|
||||
if (!this._items?.length) return;
|
||||
return html`${repeat(
|
||||
this._items,
|
||||
(item) => item.unique,
|
||||
@@ -177,20 +198,20 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(item: UmbMediaItemModel) {
|
||||
// TODO: `file-ext` value has been hardcoded here. Find out if API model has value for it. [LK]
|
||||
#renderItem(item: UmbMediaCardItemModel) {
|
||||
return html`
|
||||
<uui-card-media
|
||||
name=${ifDefined(item.name === null ? undefined : item.name)}
|
||||
detail=${ifDefined(item.unique)}
|
||||
file-ext="jpg">
|
||||
<uui-card-media name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.unique)}>
|
||||
${item.url
|
||||
? html`<img src=${item.url} alt=${item.name} />`
|
||||
: html`<umb-icon name=${ifDefined(item.icon)}></umb-icon>`}
|
||||
${this.#renderIsTrashed(item)}
|
||||
<uui-action-bar slot="actions">
|
||||
${this.#renderOpenButton(item)}
|
||||
<uui-button label="Copy media">
|
||||
<uui-button label="Copy media" look="secondary">
|
||||
<uui-icon name="icon-documents"></uui-icon>
|
||||
</uui-button>
|
||||
<uui-button
|
||||
look="secondary"
|
||||
@click=${() => this.#pickerContext.requestRemoveItem(item.unique)}
|
||||
label="Remove media ${item.name}">
|
||||
<uui-icon name="icon-trash"></uui-icon>
|
||||
@@ -200,7 +221,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
`;
|
||||
}
|
||||
|
||||
#renderIsTrashed(item: UmbMediaItemModel) {
|
||||
#renderIsTrashed(item: UmbMediaCardItemModel) {
|
||||
if (!item.isTrashed) return;
|
||||
return html`
|
||||
<uui-tag size="s" slot="tag" color="danger">
|
||||
@@ -209,7 +230,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
`;
|
||||
}
|
||||
|
||||
#renderOpenButton(item: UmbMediaItemModel) {
|
||||
#renderOpenButton(item: UmbMediaCardItemModel) {
|
||||
if (!this.showOpenButton) return;
|
||||
return html`
|
||||
<uui-button
|
||||
@@ -223,6 +244,9 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--uui-size-space-3);
|
||||
@@ -240,6 +264,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
uui-card-media umb-icon {
|
||||
font-size: var(--uui-size-8);
|
||||
}
|
||||
|
||||
uui-card-media[drag-placeholder] {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
export interface UmbUploadableFileModel extends UmbTemporaryFileModel {
|
||||
unique: string;
|
||||
file: File;
|
||||
mediaTypeUnique: string;
|
||||
}
|
||||
|
||||
@@ -38,7 +37,10 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
#mediaTypeStructure = new UmbMediaTypeStructureRepository(this);
|
||||
#mediaDetailRepository = new UmbMediaDetailRepository(this);
|
||||
|
||||
#completed = new UmbArrayState<UmbUploadableFileModel | UmbTemporaryFileModel>([], (upload) => upload.unique);
|
||||
#completed = new UmbArrayState<UmbUploadableFileModel | UmbTemporaryFileModel>(
|
||||
[],
|
||||
(upload) => upload.temporaryUnique,
|
||||
);
|
||||
public readonly completed = this.#completed.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
@@ -56,7 +58,7 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
const temporaryFiles: Array<UmbTemporaryFileModel> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const uploaded = await this.#tempFileManager.uploadOne({ unique: UmbId.new(), file });
|
||||
const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: UmbId.new(), file });
|
||||
this.#completed.setValue([...this.#completed.getValue(), uploaded]);
|
||||
temporaryFiles.push(uploaded);
|
||||
}
|
||||
@@ -107,7 +109,12 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
// Since we are uploading multiple files, we will pick first allowed option.
|
||||
// Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown?
|
||||
const mediaType = options[0];
|
||||
uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique });
|
||||
uploadableFiles.push({
|
||||
temporaryUnique: UmbId.new(),
|
||||
file,
|
||||
mediaTypeUnique: mediaType.unique,
|
||||
unique: UmbId.new(),
|
||||
});
|
||||
}
|
||||
|
||||
notAllowedFiles.forEach((file) => {
|
||||
@@ -142,6 +149,7 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
// Only one allowed option, upload file using that option.
|
||||
const uploadableFile: UmbUploadableFileModel = {
|
||||
unique: UmbId.new(),
|
||||
temporaryUnique: UmbId.new(),
|
||||
file,
|
||||
mediaTypeUnique: mediaTypes[0].unique,
|
||||
};
|
||||
@@ -156,6 +164,7 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
|
||||
const uploadableFile: UmbUploadableFileModel = {
|
||||
unique: UmbId.new(),
|
||||
temporaryUnique: UmbId.new(),
|
||||
file,
|
||||
mediaTypeUnique: mediaType.unique,
|
||||
};
|
||||
@@ -211,6 +220,7 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
if (upload.status === TemporaryFileStatus.SUCCESS) {
|
||||
// Upload successful. Create media item.
|
||||
const preset: Partial<UmbMediaDetailModel> = {
|
||||
unique: file.unique,
|
||||
mediaType: {
|
||||
unique: upload.mediaTypeUnique,
|
||||
collection: null,
|
||||
@@ -227,7 +237,7 @@ export class UmbDropzoneManager extends UmbControllerBase {
|
||||
values: [
|
||||
{
|
||||
alias: 'umbracoFile',
|
||||
value: { temporaryFileId: upload.unique },
|
||||
value: { temporaryFileId: upload.temporaryUnique },
|
||||
culture: null,
|
||||
segment: null,
|
||||
},
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import { UmbDropzoneManager } from './dropzone-manager.class.js';
|
||||
import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js';
|
||||
import { UmbProgressEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
|
||||
|
||||
@customElement('umb-dropzone')
|
||||
export class UmbDropzoneElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
parentUnique: string | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
multiple: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
createAsTemporary: boolean = false;
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
accept: Array<string> = [];
|
||||
|
||||
//TODO: logic to disable the dropzone?
|
||||
|
||||
#files: Array<UmbUploadableFileModel | UmbTemporaryFileModel> = [];
|
||||
|
||||
public getFiles() {
|
||||
return this.#files;
|
||||
}
|
||||
|
||||
public browse() {
|
||||
const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement;
|
||||
return element.browse();
|
||||
@@ -28,7 +46,9 @@ export class UmbDropzoneElement extends UmbLitElement {
|
||||
document.removeEventListener('drop', this.#handleDrop.bind(this));
|
||||
}
|
||||
|
||||
#handleDragEnter() {
|
||||
#handleDragEnter(e: DragEvent) {
|
||||
// Avoid collision with UmbSorterController
|
||||
if (!e.dataTransfer?.types?.length) return;
|
||||
this.toggleAttribute('dragging', true);
|
||||
}
|
||||
|
||||
@@ -57,23 +77,28 @@ export class UmbDropzoneElement extends UmbLitElement {
|
||||
this.dispatchEvent(new UmbProgressEvent(progress));
|
||||
|
||||
if (completed.length === files.length) {
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
this.#files = completed;
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: { completed } }));
|
||||
dropzoneManager.destroy();
|
||||
}
|
||||
},
|
||||
'_observeCompleted',
|
||||
);
|
||||
//TODO Create some placeholder items while files are being uploaded? Could update them as they get completed.
|
||||
await dropzoneManager.createFilesAsMedia(files, this.parentUnique);
|
||||
if (this.createAsTemporary) {
|
||||
await dropzoneManager.createFilesAsTemporary(files);
|
||||
} else {
|
||||
await dropzoneManager.createFilesAsMedia(files, this.parentUnique);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-file-dropzone
|
||||
id="dropzone"
|
||||
multiple
|
||||
.accept=${this.accept?.join(',')}
|
||||
?multiple=${this.multiple}
|
||||
@change=${this.#onDropFiles}
|
||||
label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}"
|
||||
accept=""></uui-file-dropzone>`;
|
||||
label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}"></uui-file-dropzone>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
|
||||
import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js';
|
||||
import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js';
|
||||
import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js';
|
||||
@@ -6,8 +5,10 @@ import type { UmbMediaCardItemModel, UmbMediaPathModel } from './types.js';
|
||||
import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js';
|
||||
import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
|
||||
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
|
||||
|
||||
@@ -63,11 +64,21 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
|
||||
async #mapMediaUrls(items: Array<UmbMediaItemModel>): Promise<Array<UmbMediaCardItemModel>> {
|
||||
if (!items.length) return [];
|
||||
|
||||
const { data } = await this.#imagingRepository.requestResizedItems(items.map((item) => item.unique));
|
||||
const { data } = await this.#imagingRepository.requestResizedItems(
|
||||
items.map((item) => item.unique),
|
||||
{ height: 400, width: 400, mode: ImageCropModeModel.MIN },
|
||||
);
|
||||
|
||||
return items.map((item): UmbMediaCardItemModel => {
|
||||
const url = data?.find((media) => media.unique === item.unique)?.url;
|
||||
return { name: item.name, unique: item.unique, url, icon: item.mediaType.icon, entityType: item.entityType };
|
||||
return {
|
||||
name: item.name,
|
||||
unique: item.unique,
|
||||
url,
|
||||
icon: item.mediaType.icon,
|
||||
entityType: item.entityType,
|
||||
isTrashed: item.isTrashed,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +247,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
|
||||
padding-bottom: 5px; /** The modal is a bit jumpy due to the img card focus/hover border. This fixes the issue. */
|
||||
}
|
||||
umb-icon {
|
||||
font-size: var(--uui-size-24);
|
||||
font-size: var(--uui-size-8);
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
@@ -5,8 +5,9 @@ export interface UmbMediaCardItemModel {
|
||||
name: string;
|
||||
unique: string;
|
||||
entityType: UmbMediaEntityType;
|
||||
isTrashed: boolean;
|
||||
icon: string;
|
||||
url?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface UmbMediaPathModel extends UmbEntityModel {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
@property({ attribute: false })
|
||||
value: UmbImageCropperPropertyEditorValue = {
|
||||
temporaryFileId: null,
|
||||
src: '',
|
||||
crops: [],
|
||||
focalPoint: { left: 0.5, top: 0.5 },
|
||||
@@ -28,6 +29,7 @@ export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implem
|
||||
if (changedProperties.has('value')) {
|
||||
if (!this.value) {
|
||||
this.value = {
|
||||
temporaryFileId: null,
|
||||
src: '',
|
||||
crops: [],
|
||||
focalPoint: { left: 0.5, top: 0.5 },
|
||||
|
||||
Reference in New Issue
Block a user