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:
Lone Iversen
2023-04-03 09:37:32 +02:00
committed by GitHub
parent 447c833bf8
commit 0380d5c054
6 changed files with 239 additions and 6 deletions

View File

@@ -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';

View File

@@ -26,7 +26,7 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement)
}
#add-button {
text-align: center;
height: 202px;
height: 160px;
}
uui-icon {

View File

@@ -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;
}
}

View File

@@ -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,
},
};

View File

@@ -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>`;
}
}

View File

@@ -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',