V15: Show server configuration when configuring the Upload Field (#18185)

* feat: shows notification when no suitable media type is found

* chore: rearrange imports

* feat: use a forward ref to find the dropzone

* chore: rearrange imports

* chore(mock): send back correct header

* feat: avoid using the context consumer to get a token, but instead mimick the OpenAPI generator

* chore(mock): allow more file types

* chore(mock): create more upload fields

* chore(mock): also look for mediaPicker fields

* chore(mock): improve media mock db

* chore(mock): add missing endpoints

* chore(mock): update media data

* chore(mock): fix aliases for media grid and table

* chore(mock): add urls to media

* chore(mock): adds missing endpoint for imaging

* fix: reverse order of properties to overwrite existing status

* feat: listen to progress updates on upload and update the `progress` property

* feat: adds tracking of upload progress to placeholders

* feat: bind the progress number up on the temporary file badge to indicate upload status

* feat: optimises progress calculation and makes the badge bigger to be able to show the progress in percent

* feat: allow text to be normal

* chore: use correct localization

* feat: shows error status for anything that isn't waiting or complete

* feat: makes `progress` optional

* feat: adds repository+store for temporary file configuration

* chore(mock): adds mock endpoint for temporary file configuration

* feat: set progress for createTemporaryFiles

* feat: allows a `whitespace` option to notifications

* feat: validates uploads before trying to query the server

* feat: adds `formatBytes` function to format numbers

* chore: export all consts

* feat: exports bytes function

* feat: set decimals to default to 2, which works nicely with the Intl numberformat

* feat: use `formatBytes` to format the error message

* chore(mock): set max file size for mock to 1.4 GB

* feat: adds localization

* Update src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>

* chore: add end character to comment

* feat: binds multiple text string to validation

* chore: fixes event type

* feat: adds new property editor ui for accepted file types

* feat: changes the upload field to use the property editor ui for accepted file types

* adds localization

* Markup/style refactoring/streamlining

* Renamed "Accepted Types" to "Accepted Upload Types"

---------

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
This commit is contained in:
Jacob Overgaard
2025-02-03 14:57:55 +01:00
committed by GitHub
parent ee231c7bae
commit f1cdf50cdd
10 changed files with 251 additions and 15 deletions

View File

@@ -372,6 +372,8 @@ export default {
fileSecurityValidationFailure: 'One or more file security validations have failed',
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
uploadNotAllowed: 'Upload is not allowed in this location.',
noticeExtensionsServerOverride:
'Regardless of the allowed file types, the following limitations apply system-wide due to the server configuration:',
},
member: {
'2fa': 'Two-Factor Authentication',
@@ -885,6 +887,7 @@ export default {
retrieve: 'Retrieve',
retry: 'Retry',
rights: 'Permissions',
serverConfiguration: 'Server Configuration',
scheduledPublishing: 'Scheduled Publishing',
umbracoInfo: 'Umbraco info',
search: 'Search',
@@ -2138,6 +2141,9 @@ export default {
numberMinimum: "Value must be greater than or equal to '%0%'.",
numberMaximum: "Value must be less than or equal to '%0%'.",
numberMisconfigured: "Minimum value '%0%' must be less than the maximum value '%1%'.",
invalidExtensions: 'One or more of the extensions are invalid.',
allowedExtensions: 'Allowed extensions are:',
disallowedExtensions: 'Disallowed extensions are:',
},
healthcheck: {
checkSuccessMessage: "Value is set to the recommended value: '%0%'.",

View File

@@ -65,12 +65,12 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U
}
// Prevent valid events from bubbling outside the message element
#onValid(event: any) {
#onValid(event: Event) {
event.stopPropagation();
}
// Prevent invalid events from bubbling outside the message element
#onInvalid(event: any) {
#onInvalid(event: Event) {
event.stopPropagation();
}

View File

@@ -11,7 +11,7 @@ export const manifest: ManifestPropertyEditorSchema = {
{
alias: 'fileExtensions',
label: 'Accepted file extensions',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.AcceptedUploadTypes',
},
],
},

View File

@@ -0,0 +1 @@
export * from './property-editor-ui-accepted-upload-types.element.js';

View File

@@ -0,0 +1,15 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.AcceptedUploadTypes',
name: 'Accepted Upload Types Property Editor UI',
element: () => import('./property-editor-ui-accepted-upload-types.element.js'),
meta: {
label: 'Accepted Upload Types',
propertyEditorSchemaAlias: 'Umbraco.MultipleTextstring',
icon: 'icon-ordered-list',
group: 'lists',
supportsReadOnly: true,
},
};

View File

@@ -0,0 +1,143 @@
import { UmbPropertyEditorUIMultipleTextStringElement } from '../multiple-text-string/property-editor-ui-multiple-text-string.element.js';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { formatBytes } from '@umbraco-cms/backoffice/utils';
import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
import type { UmbTemporaryFileConfigurationModel } from '@umbraco-cms/backoffice/temporary-file';
/**
* @element umb-property-editor-ui-accepted-upload-types
*/
@customElement('umb-property-editor-ui-accepted-upload-types')
export class UmbPropertyEditorUIAcceptedUploadTypesElement
extends UmbPropertyEditorUIMultipleTextStringElement
implements UmbPropertyEditorUiElement
{
#temporaryFileConfigRepository = new UmbTemporaryFileConfigRepository(this);
@state()
protected _acceptedTypes: string[] = [];
@state()
protected _disallowedTypes: string[] = [];
@state()
protected _maxFileSize?: number | null;
override async connectedCallback() {
super.connectedCallback();
await this.#temporaryFileConfigRepository.initialized;
this.observe(this.#temporaryFileConfigRepository.all(), (config) => {
if (!config) return;
this.#addValidators(config);
this._acceptedTypes = config.allowedUploadedFileExtensions;
this._disallowedTypes = config.disallowedUploadedFilesExtensions;
this._maxFileSize = config.maxFileSize ? config.maxFileSize * 1024 : null;
});
}
#addValidators(config: UmbTemporaryFileConfigurationModel) {
this._inputElement?.addValidator(
'badInput',
() => {
let message = this.localize.term('validation_invalidExtensions');
if (config.allowedUploadedFileExtensions.length) {
message += `<br>${this.localize.term('validation_allowedExtensions')} ${config.allowedUploadedFileExtensions.join(', ')}`;
}
if (config.disallowedUploadedFilesExtensions.length) {
message += `<br>${this.localize.term('validation_disallowedExtensions')} ${config.disallowedUploadedFilesExtensions.join(', ')}`;
}
return message;
},
() => {
const extensions = this._inputElement?.items;
if (!extensions) return false;
if (
config.allowedUploadedFileExtensions.length &&
!config.allowedUploadedFileExtensions.some((ext) => extensions.includes(ext))
) {
return true;
}
if (config.disallowedUploadedFilesExtensions.some((ext) => extensions.includes(ext))) {
return true;
}
return false;
},
);
}
#renderAcceptedTypes() {
if (!this._acceptedTypes.length && !this._disallowedTypes.length && !this._maxFileSize) {
return nothing;
}
return html`
<uui-box id="notice" headline=${this.localize.term('general_serverConfiguration')}>
<p><umb-localize key="media_noticeExtensionsServerOverride"></umb-localize></p>
${when(
this._acceptedTypes.length,
() => html`
<p>
<umb-localize key="validation_allowedExtensions"></umb-localize>
<strong>${this._acceptedTypes.join(', ')}</strong>
</p>
`,
)}
${when(
this._disallowedTypes.length,
() => html`
<p>
<umb-localize key="validation_disallowedExtensions"></umb-localize>
<strong>${this._disallowedTypes.join(', ')}</strong>
</p>
`,
)}
${when(
this._maxFileSize,
() => html`
<p>
${this.localize.term('media_maxFileSize')}
<strong title="${this.localize.number(this._maxFileSize!)} bytes"
>${formatBytes(this._maxFileSize!, { decimals: 2 })}</strong
>.
</p>
`,
)}
</uui-box>
`;
}
override render() {
return html`${this.#renderAcceptedTypes()} ${super.render()}`;
}
static override readonly styles = [
css`
#notice {
--uui-box-default-padding: var(--uui-size-space-4);
--uui-box-header-padding: var(--uui-size-space-4);
--uui-color-divider-standalone: var(--uui-color-warning-standalone);
border: 1px solid var(--uui-color-divider-standalone);
background-color: var(--uui-color-warning);
color: var(--uui-color-warning-contrast);
margin-bottom: var(--uui-size-layout-1);
p {
margin: 0.5rem 0;
}
}
`,
];
}
export default UmbPropertyEditorUIAcceptedUploadTypesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-accepted-upload-types': UmbPropertyEditorUIAcceptedUploadTypesElement;
}
}

View File

@@ -0,0 +1,15 @@
import type { UmbPropertyEditorUIAcceptedUploadTypesElement } from './property-editor-ui-accepted-upload-types.element.js';
import type { Meta, StoryFn } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-editor-ui-accepted-types.element.js';
export default {
title: 'Property Editor UIs/Accepted Types',
component: 'umb-property-editor-ui-accepted-types',
id: 'umb-property-editor-ui-accepted-types',
} as Meta;
export const AAAOverview: StoryFn<UmbPropertyEditorUIAcceptedUploadTypesElement> = () =>
html`<umb-property-editor-ui-accepted-upload-types></umb-property-editor-ui-accepted-upload-types>`;
AAAOverview.storyName = 'Overview';

View File

@@ -0,0 +1,23 @@
import { UmbPropertyEditorUIAcceptedUploadTypesElement } from './property-editor-ui-accepted-upload-types.element.js';
import { expect, fixture, html } from '@open-wc/testing';
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
describe('UmbPropertyEditorUIUploadFieldElement', () => {
let element: UmbPropertyEditorUIAcceptedUploadTypesElement;
beforeEach(async () => {
element = await fixture(html`
<umb-property-editor-ui-accepted-upload-types></umb-property-editor-ui-accepted-upload-types>
`);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbPropertyEditorUIAcceptedUploadTypesElement);
});
if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
});
}
});

View File

@@ -1,3 +1,4 @@
import { manifest as acceptedType } from './accepted-types/manifests.js';
import { manifest as colorEditor } from './color-swatches-editor/manifests.js';
import { manifest as numberRange } from './number-range/manifests.js';
import { manifest as orderDirection } from './order-direction/manifests.js';
@@ -38,6 +39,7 @@ export const manifests: Array<UmbExtensionManifest> = [
...textBoxManifests,
...toggleManifests,
...contentPickerManifests,
acceptedType,
colorEditor,
numberRange,
orderDirection,

View File

@@ -1,13 +1,18 @@
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import {
UMB_SUBMITTABLE_WORKSPACE_CONTEXT,
UmbSubmittableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components';
import type {
UmbPropertyEditorConfigCollection,
UmbPropertyEditorUiElement,
} from '@umbraco-cms/backoffice/property-editor';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
/**
* @element umb-property-editor-ui-multiple-text-string
@@ -60,11 +65,23 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
@state()
private _max = Infinity;
@query('#input', true)
protected _inputElement?: UmbInputMultipleTextStringElement;
protected _validationContext = new UmbValidationContext(this);
constructor() {
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this._label = context.getLabel();
});
this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (context) => {
if (context instanceof UmbSubmittableWorkspaceContextBase) {
context.addValidationContext(this._validationContext);
}
});
}
protected override firstUpdated() {
@@ -83,17 +100,31 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
// Prevent valid events from bubbling outside the message element
#onValid(event: Event) {
event.stopPropagation();
}
// Prevent invalid events from bubbling outside the message element
#onInvalid(event: Event) {
event.stopPropagation();
}
override render() {
return html`
<umb-input-multiple-text-string
max=${this._max}
min=${this._min}
.items=${this.value ?? []}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
@change=${this.#onChange}>
</umb-input-multiple-text-string>
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
<umb-input-multiple-text-string
id="input"
max=${this._max}
min=${this._min}
.items=${this.value ?? []}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
@change=${this.#onChange}
${umbBindToValidation(this)}>
</umb-input-multiple-text-string>
</umb-form-validation-message>
`;
}
}