Feature/property editor upload field (#583)
* init * change event * handle multiple files and validation * render correction * correct import * story --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import './input-media-picker/input-media-picker.element';
|
||||
import './input-multi-url-picker/input-multi-url-picker.element';
|
||||
import './input-slider/input-slider.element';
|
||||
import './input-toggle/input-toggle.element';
|
||||
import './input-upload-field/input-upload-field.element';
|
||||
import './input-template-picker/input-template-picker.element';
|
||||
import './property-type-based-property/property-type-based-property.element';
|
||||
import './ref-property-editor-ui/ref-property-editor-ui.element';
|
||||
|
||||
@@ -26,7 +26,7 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement)
|
||||
}
|
||||
#add-button {
|
||||
text-align: center;
|
||||
height: 202px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
uui-icon {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { UUIFileDropzoneElement } from '@umbraco-ui/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
@customElement('umb-input-upload-field')
|
||||
export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
uui-icon {
|
||||
vertical-align: sub;
|
||||
margin-right: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
uui-symbol-file-thumbnail {
|
||||
box-sizing: border-box;
|
||||
min-height: 150px;
|
||||
padding: var(--uui-size-space-4);
|
||||
border: 1px solid var(--uui-color-border);
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, auto));
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private _keys: Array<string> = [];
|
||||
/**
|
||||
* @description Keys to the files that belong to this upload field.
|
||||
* @type {Array<String>}
|
||||
* @default []
|
||||
*/
|
||||
@property({ type: Array<string> })
|
||||
public set keys(fileKeys: Array<string>) {
|
||||
this._keys = fileKeys;
|
||||
super.value = this._keys.join(',');
|
||||
}
|
||||
public get keys(): Array<string> {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Allowed file extensions. If left empty, all are allowed.
|
||||
* @type {Array<String>}
|
||||
* @default undefined
|
||||
*/
|
||||
@property({ type: Array<string> })
|
||||
fileExtensions?: Array<string>;
|
||||
|
||||
/**
|
||||
* @description Allows the user to upload multiple files.
|
||||
* @type {Boolean}
|
||||
* @default false
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
multiple = false;
|
||||
|
||||
@state()
|
||||
_currentFiles: Blob[] = [];
|
||||
|
||||
@state()
|
||||
_currentFilesTemp?: Blob[];
|
||||
|
||||
@state()
|
||||
extensions?: string[];
|
||||
|
||||
@query('#dropzone')
|
||||
private _dropzone?: UUIFileDropzoneElement;
|
||||
|
||||
private _notificationContext?: UmbNotificationContext;
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
|
||||
this._notificationContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.#setExtensions();
|
||||
}
|
||||
|
||||
#setExtensions() {
|
||||
if (!this.fileExtensions?.length) return;
|
||||
|
||||
this.extensions = this.fileExtensions.map((extension) => {
|
||||
return `.${extension}`;
|
||||
});
|
||||
}
|
||||
|
||||
#onUpload(e: CustomEvent) {
|
||||
// UUIFileDropzoneEvent doesnt exist?
|
||||
|
||||
this._currentFilesTemp = e.detail.files;
|
||||
|
||||
if (!this.fileExtensions?.length && this._currentFilesTemp?.length) {
|
||||
this.#setFiles(this._currentFilesTemp);
|
||||
return;
|
||||
}
|
||||
const validated = this.#validateExtensions();
|
||||
this.#setFiles(validated);
|
||||
}
|
||||
|
||||
#validateExtensions(): Blob[] {
|
||||
// TODO: Should property editor be able to handle allowed extensions like image/* ?
|
||||
|
||||
const filesValidated: Blob[] = [];
|
||||
this._currentFilesTemp?.forEach((temp) => {
|
||||
const type = temp.type.slice(temp.type.lastIndexOf('/') + 1, temp.length);
|
||||
if (this.fileExtensions?.find((x) => x === type)) filesValidated.push(temp);
|
||||
else
|
||||
this._notificationContext?.peek('danger', {
|
||||
data: { headline: 'File upload', message: `Chosen file type "${type}" is not allowed` },
|
||||
});
|
||||
});
|
||||
|
||||
return filesValidated;
|
||||
}
|
||||
#setFiles(files: Blob[]) {
|
||||
this._currentFiles = [...this._currentFiles, ...files];
|
||||
|
||||
//TODO: set keys when possible, not names
|
||||
this.keys = this._currentFiles.map((file) => file.name);
|
||||
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
#handleBrowse() {
|
||||
if (!this._dropzone) return;
|
||||
this._dropzone.browse();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`${this.#renderFiles()} ${this.#renderDropzone()}`;
|
||||
}
|
||||
|
||||
#renderDropzone() {
|
||||
if (!this.multiple && this._currentFiles.length) return nothing;
|
||||
return html`
|
||||
<uui-file-dropzone
|
||||
id="dropzone"
|
||||
label="dropzone"
|
||||
@file-change="${this.#onUpload}"
|
||||
accept="${ifDefined(this.extensions?.join(', '))}"
|
||||
?multiple="${this.multiple}">
|
||||
<uui-button label="upload" @click="${this.#handleBrowse}">Upload file here</uui-button>
|
||||
</uui-file-dropzone>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderFiles() {
|
||||
if (!this._currentFiles?.length) return nothing;
|
||||
return html` <div id="wrapper">
|
||||
${this._currentFiles.map((file) => {
|
||||
return html` <uui-symbol-file-thumbnail src="${ifDefined(file.name)}" alt="${ifDefined(file.name)}">
|
||||
</uui-symbol-file-thumbnail>`;
|
||||
})}
|
||||
</div>
|
||||
<uui-button compact @click="${this.#handleRemove}" label="Remove files">
|
||||
<uui-icon name="umb:trash"></uui-icon> Remove file(s)
|
||||
</uui-button>`;
|
||||
}
|
||||
|
||||
#handleRemove() {
|
||||
// Remove via endpoint?
|
||||
this._currentFiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbInputUploadFieldElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-upload-field': UmbInputUploadFieldElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Meta, StoryObj } from '@storybook/web-components';
|
||||
import './input-upload-field.element';
|
||||
import type { UmbInputUploadFieldElement } from './input-upload-field.element';
|
||||
|
||||
const meta: Meta<UmbInputUploadFieldElement> = {
|
||||
title: 'Components/Inputs/Upload Field',
|
||||
component: 'umb-input-upload-field',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<UmbInputUploadFieldElement>;
|
||||
|
||||
export const Overview: Story = {
|
||||
args: {
|
||||
multiple: false,
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { html } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbPropertyEditorElement } from '@umbraco-cms/backoffice/property-editor';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { UmbInputUploadFieldElement } from '../../../../shared/components/input-upload-field/input-upload-field.element';
|
||||
import type { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import type { UmbPropertyEditorElement } from '@umbraco-cms/backoffice/property-editor';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
|
||||
/**
|
||||
@@ -15,10 +17,30 @@ export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement impleme
|
||||
value = '';
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public config = [];
|
||||
public set config(config: Array<DataTypePropertyPresentationModel>) {
|
||||
const fileExtensions = config.find((x) => x.alias === 'fileExtensions');
|
||||
if (fileExtensions) this._fileExtensions = fileExtensions.value;
|
||||
|
||||
const multiple = config.find((x) => x.alias === 'multiple');
|
||||
if (multiple) this._multiple = multiple.value;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _fileExtensions?: Array<string>;
|
||||
|
||||
@state()
|
||||
private _multiple?: boolean;
|
||||
|
||||
private _onChange(event: CustomEvent) {
|
||||
this.value = (event.target as unknown as UmbInputUploadFieldElement).value as string;
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>umb-property-editor-ui-upload-field</div>`;
|
||||
return html`<umb-input-upload-field
|
||||
@change="${this._onChange}"
|
||||
?multiple="${this._multiple}"
|
||||
.fileExtensions="${this._fileExtensions}"></umb-input-upload-field>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -464,7 +464,12 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde
|
||||
parentKey: null,
|
||||
propertyEditorAlias: 'Umbraco.UploadField',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUI.UploadField',
|
||||
values: [],
|
||||
values: [
|
||||
{
|
||||
alias: 'fileExtensions',
|
||||
value: ['jpg', 'jpeg', 'png'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
$type: 'data-type',
|
||||
|
||||
Reference in New Issue
Block a user