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:
@@ -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%'.",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const manifest: ManifestPropertyEditorSchema = {
|
||||
{
|
||||
alias: 'fileExtensions',
|
||||
label: 'Accepted file extensions',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUi.AcceptedUploadTypes',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './property-editor-ui-accepted-upload-types.element.js';
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user