Merge branch 'v15/dev' into v15/bugfix/18000

This commit is contained in:
Niels Lyngsø
2025-02-03 15:49:30 +01:00
committed by GitHub
15 changed files with 325 additions and 62 deletions

View File

@@ -822,6 +822,7 @@ export default {
error: 'Error',
field: 'Field',
fieldFor: 'Field for %0%',
toggleFor: 'Toggle for %0%',
findDocument: 'Find',
first: 'First',
focalPoint: 'Focal point',

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',
@@ -817,6 +819,7 @@ export default {
error: 'Error',
field: 'Field',
fieldFor: 'Field for %0%',
toggleFor: 'Toggle for %0%',
findDocument: 'Find',
first: 'First',
focalPoint: 'Focal point',
@@ -885,6 +888,7 @@ export default {
retrieve: 'Retrieve',
retry: 'Retry',
rights: 'Permissions',
serverConfiguration: 'Server Configuration',
scheduledPublishing: 'Scheduled Publishing',
umbracoInfo: 'Umbraco info',
search: 'Search',
@@ -2138,6 +2142,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

@@ -26,6 +26,9 @@ export class UmbInputToggleElement extends UUIFormControlMixin(UmbLitElement, ''
@property({ type: String })
labelOff?: string;
@property({ type: String, attribute: 'aria-label' })
override ariaLabel: string | null = null;
/**
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
* @type {boolean}
@@ -42,6 +45,8 @@ export class UmbInputToggleElement extends UUIFormControlMixin(UmbLitElement, ''
return undefined;
}
// test
override connectedCallback(): void {
super.connectedCallback();
this.#updateLabel();
@@ -54,15 +59,15 @@ export class UmbInputToggleElement extends UUIFormControlMixin(UmbLitElement, ''
}
#updateLabel() {
this._currentLabel = this.showLabels ? (this.checked ? this.labelOn : this.labelOff) : '';
this._currentLabel = this.showLabels ? (this.checked ? this.labelOn : this.labelOff) : '';
}
override render() {
return html`<uui-toggle
.checked=${this.#checked}
.label=${this._currentLabel}
.label="${this.ariaLabel}"
@change=${this.#onChange}
?readonly=${this.readonly}></uui-toggle>`;
?readonly=${this.readonly}><span>${this._currentLabel}</span> </uui-toggle>`;
}
static override styles = [

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

@@ -1,11 +1,15 @@
import { html, customElement, property, state, css, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-file')
export default class UmbInputUploadFieldFileElement extends UmbLitElement {
@property({ type: String })
#loadingText = `(${this.localize.term('general_loading')}...)`;
#serverUrl = '';
@property()
path: string = '';
/**
@@ -22,53 +26,39 @@ export default class UmbInputUploadFieldFileElement extends UmbLitElement {
@state()
label = '';
#serverUrl = '';
#loadingText = `(${this.localize.term('general_loading')}...)`;
/**
*
*/
constructor() {
super();
this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.#serverUrl = instance.getServerUrl();
}).asPromise();
});
}
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('file') && this.file) {
this.extension = this.file.name.split('.').pop() ?? '';
this.label = this.file.name || this.#loadingText;
}
if (_changedProperties.has('path')) {
if (this.#serverUrl) {
if (this.file) return;
this.extension = this.path.split('.').pop() ?? '';
this.label = this.#serverUrl ? this.path.substring(this.#serverUrl.length) : this.#loadingText;
}
if (_changedProperties.has('path') && !this.file) {
this.extension = this.path.split('.').pop() ?? '';
this.label = this.path.split('/').pop() ?? this.#loadingText;
}
}
#renderLabel() {
if (this.path) {
// Don't make it a link if it's a temp file upload.
return this.file ? this.label : html`<a id="label" href=${this.path} target="_blank">${this.label}</a>`;
}
return html`<span id="label">${this.label}</span>`;
}
override render() {
if (!this.label && !this.extension) return html`<uui-loader></uui-loader>`;
return html`
<div id="main">
<uui-symbol-file id="file-symbol" .type=${this.extension}></uui-symbol-file>
${this.#renderLabel()}
${when(
!this.file && this.path,
() => html`<a id="label" href="${this.#serverUrl}${this.path}" target="_blank">${this.label}</a>`,
() => html`<span id="label">${this.label}</span>`,
)}
</div>
`;
}
@@ -81,26 +71,30 @@ export default class UmbInputUploadFieldFileElement extends UmbLitElement {
box-sizing: border-box;
color: var(--uui-color-text);
}
#file-symbol {
aspect-ratio: 1 / 1;
margin: auto;
max-width: 100%;
max-height: 100%;
}
#label {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--uui-color-text);
}
a#label {
text-decoration: none;
color: var(--uui-color-interactive);
}
a#label:hover {
text-decoration: underline;
color: var(--uui-color-interactive-emphasis);
&:is(a) {
text-decoration: none;
color: var(--uui-color-interactive);
&:hover {
text-decoration: underline;
color: var(--uui-color-interactive-emphasis);
}
}
}
`,
];

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

View File

@@ -16,27 +16,43 @@ export const manifests: Array<UmbExtensionManifest> = [
properties: [
{
alias: 'default',
label: 'Initial State',
description:
'The initial state for the toggle, when it is displayed for the first time in the backoffice, eg. for a new content item.',
label: 'Preset value',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
config: [
{
alias: "ariaLabel",
value: 'toggle for the initial state of this data type'
}
]
},
{
alias: 'showLabels',
label: 'Show toggle labels',
description: 'Show labels next to toggle button.',
label: 'Show on/off labels',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
config: [
{
alias: "ariaLabel",
value: 'toggle for weather if label should be displayed'
}
]
},
{
alias: 'labelOn',
label: 'Label On',
description: 'Label text when enabled.',
description: 'Displays text when enabled.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox',
},
{
alias: 'labelOff',
label: 'Label Off',
description: 'Label text when disabled.',
description: 'Displays text when disabled.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox',
},
{
alias: 'ariaLabel',
label: 'Screen Reader Label',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox',
},
],

View File

@@ -24,21 +24,30 @@ export class UmbPropertyEditorUIToggleElement extends UmbLitElement implements U
@property({ type: Boolean, reflect: true })
readonly = false;
@state()
_ariaLabel?: string;
@state()
_labelOff?: string;
@state()
_labelOn?: string;
@property({ type: String })
name?: string;
@state()
_showLabels = false;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
this.value ??= config.getValueByAlias('default') ?? false;
this._labelOff = config.getValueByAlias('labelOff');
this._labelOn = config.getValueByAlias('labelOn');
this._showLabels = Boolean(config.getValueByAlias('showLabels'));
this._ariaLabel = config.getValueByAlias('ariaLabel');
}
#onChange(event: CustomEvent & { target: UmbInputToggleElement }) {
@@ -49,7 +58,8 @@ export class UmbPropertyEditorUIToggleElement extends UmbLitElement implements U
override render() {
return html`
<umb-input-toggle
.labelOn=${this._labelOn}
.ariaLabel=${this._ariaLabel ? this.localize.string(this._ariaLabel) : this.localize.term('general_toggleFor', [this.name])}
.labelOff=${this._labelOff}
?checked=${this.value}
?showLabels=${this._showLabels}