diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index b6e9b4d434..7a9fc1c76d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -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%'.", diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts index d2f07b510f..aca689e71c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts @@ -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(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts index fe697d8989..ba74e968d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/upload-field/Umbraco.UploadField.ts @@ -11,7 +11,7 @@ export const manifest: ManifestPropertyEditorSchema = { { alias: 'fileExtensions', label: 'Accepted file extensions', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.AcceptedUploadTypes', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts new file mode 100644 index 0000000000..0d1a4e6250 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/index.ts @@ -0,0 +1 @@ +export * from './property-editor-ui-accepted-upload-types.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts new file mode 100644 index 0000000000..a88774c861 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/manifests.ts @@ -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, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.element.ts new file mode 100644 index 0000000000..9a85fa2b83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.element.ts @@ -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 += `
${this.localize.term('validation_allowedExtensions')} ${config.allowedUploadedFileExtensions.join(', ')}`; + } + if (config.disallowedUploadedFilesExtensions.length) { + message += `
${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` + +

+ ${when( + this._acceptedTypes.length, + () => html` +

+ + ${this._acceptedTypes.join(', ')} +

+ `, + )} + ${when( + this._disallowedTypes.length, + () => html` +

+ + ${this._disallowedTypes.join(', ')} +

+ `, + )} + ${when( + this._maxFileSize, + () => html` +

+ ${this.localize.term('media_maxFileSize')} + ${formatBytes(this._maxFileSize!, { decimals: 2 })}. +

+ `, + )} +
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.stories.ts new file mode 100644 index 0000000000..b2803dff05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.stories.ts @@ -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 = () => + html``; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.test.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.test.ts new file mode 100644 index 0000000000..89d065a7ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/accepted-types/property-editor-ui-accepted-upload-types.test.ts @@ -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` + + `); + }); + + 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); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts index 55595ce291..4ded362fc7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts @@ -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 = [ ...textBoxManifests, ...toggleManifests, ...contentPickerManifests, + acceptedType, colorEditor, numberRange, orderDirection, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts index 8cbeee6b93..269630d09c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts @@ -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` - - + + + + `; } }