From 0380d5c054ce76d3bc0156f3ba9bacd607c6af2e Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 3 Apr 2023 09:37:32 +0200 Subject: [PATCH] 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> --- .../src/backoffice/shared/components/index.ts | 1 + .../input-media-picker.element.ts | 2 +- .../input-upload-field.element.ts | 188 ++++++++++++++++++ .../input-upload-field.stories.ts | 17 ++ ...property-editor-ui-upload-field.element.ts | 30 ++- .../src/core/mocks/data/data-type.data.ts | 7 +- 6 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index d7c74fb3df..78b453c6cb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts index 626bfd9def..d14afe852b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts @@ -26,7 +26,7 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement) } #add-button { text-align: center; - height: 202px; + height: 160px; } uui-icon { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts new file mode 100644 index 0000000000..a5237a876d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts @@ -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 = []; + /** + * @description Keys to the files that belong to this upload field. + * @type {Array} + * @default [] + */ + @property({ type: Array }) + public set keys(fileKeys: Array) { + this._keys = fileKeys; + super.value = this._keys.join(','); + } + public get keys(): Array { + return this._keys; + } + + /** + * @description Allowed file extensions. If left empty, all are allowed. + * @type {Array} + * @default undefined + */ + @property({ type: Array }) + fileExtensions?: Array; + + /** + * @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` + + Upload file here + + `; + } + + #renderFiles() { + if (!this._currentFiles?.length) return nothing; + return html`
+ ${this._currentFiles.map((file) => { + return html` + `; + })} +
+ + Remove file(s) + `; + } + + #handleRemove() { + // Remove via endpoint? + this._currentFiles = []; + } +} + +export default UmbInputUploadFieldElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-upload-field': UmbInputUploadFieldElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.stories.ts new file mode 100644 index 0000000000..02a1fdb44f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.stories.ts @@ -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 = { + title: 'Components/Inputs/Upload Field', + component: 'umb-input-upload-field', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + multiple: false, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/upload-field/property-editor-ui-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/upload-field/property-editor-ui-upload-field.element.ts index 22b4707a98..56afaf29ca 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/upload-field/property-editor-ui-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/upload-field/property-editor-ui-upload-field.element.ts @@ -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) { + 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; + + @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`
umb-property-editor-ui-upload-field
`; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index 83e6cd42e3..ff90a90a2e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -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',