Merge branch 'main' into v14/feature/readonly-markdown-property-editor
This commit is contained in:
@@ -11,11 +11,15 @@ export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implemen
|
||||
@property({ attribute: false })
|
||||
content?: UmbBlockDataType;
|
||||
|
||||
@property({ attribute: false })
|
||||
settings?: UmbBlockDataType;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="uui-text">
|
||||
<div class="uui-text ${this.settings?.blockAlignment ? 'align-' + this.settings?.blockAlignment : undefined}">
|
||||
<h5 class="uui-text">My Custom View</h5>
|
||||
<p>Headline: ${this.content?.headline}</p>
|
||||
<p>Alignment: ${this.settings?.blockAlignment}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -31,6 +35,13 @@ export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implemen
|
||||
border-radius: 9px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ export const manifests: Array<ManifestBlockEditorCustomView> = [
|
||||
name: 'Block Editor Custom View Test',
|
||||
element: () => import('./block-custom-view.js'),
|
||||
forContentTypeAlias: 'headlineUmbracoDemoBlock',
|
||||
forBlockEditor: 'block-grid',
|
||||
forBlockEditor: 'block-list',
|
||||
},
|
||||
];
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5068,6 +5068,12 @@ export type PostUserByIdChangePasswordData = {
|
||||
|
||||
export type PostUserByIdChangePasswordResponse = string;
|
||||
|
||||
export type PostCurrentUserChangePasswordData = {
|
||||
requestBody?: ChangePasswordCurrentUserRequestModel;
|
||||
};
|
||||
|
||||
export type PostCurrentUserChangePasswordResponse = string;
|
||||
|
||||
export type PostUserByIdResetPasswordData = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -111,8 +111,6 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
manifests.forEach((manifest) => {
|
||||
const existing = this._extensions.find((x) => x.alias === manifest.alias);
|
||||
if (!existing) {
|
||||
// Idea: could be abstracted into a createController method, so we can override it in a subclass.
|
||||
// (This should be enough to be able to create a element extension controller instead.)
|
||||
this._extensions.push(this._createController(manifest));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -406,6 +406,27 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Dropdown Alignment Options',
|
||||
id: 'dt-dropdown-align',
|
||||
parent: null,
|
||||
editorAlias: 'Umbraco.DropDown.Flexible',
|
||||
editorUiAlias: 'Umb.PropertyEditorUi.Dropdown',
|
||||
hasChildren: false,
|
||||
isFolder: false,
|
||||
isDeletable: true,
|
||||
canIgnoreStartNodes: false,
|
||||
values: [
|
||||
{
|
||||
alias: 'multiple',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
alias: 'items',
|
||||
value: ['left', 'center', 'right'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Slider',
|
||||
id: 'dt-slider',
|
||||
@@ -587,6 +608,7 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
{
|
||||
label: 'Headline',
|
||||
contentElementTypeKey: 'headline-umbraco-demo-block-id',
|
||||
settingsElementTypeKey: 'headline-settings-demo-block-id',
|
||||
backgroundColor: 'gold',
|
||||
editorSize: 'medium',
|
||||
icon: 'icon-edit',
|
||||
@@ -613,7 +635,7 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
},
|
||||
{
|
||||
alias: 'useInlineEditingAsDefault',
|
||||
value: true,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
alias: 'useLiveEditing',
|
||||
|
||||
@@ -1507,6 +1507,60 @@ export const data: Array<UmbMockDocumentTypeModel> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
allowedTemplates: [],
|
||||
defaultTemplate: null,
|
||||
id: 'headline-settings-demo-block-id',
|
||||
alias: 'headlineSettingsUmbracoDemoBlock',
|
||||
name: 'Headline',
|
||||
description: null,
|
||||
icon: 'icon-edit',
|
||||
allowedAsRoot: true,
|
||||
variesByCulture: false,
|
||||
variesBySegment: false,
|
||||
isElement: true,
|
||||
hasChildren: false,
|
||||
parent: { id: 'folder-umbraco-demo-blocks-id' },
|
||||
isFolder: false,
|
||||
allowedDocumentTypes: [],
|
||||
compositions: [],
|
||||
cleanup: {
|
||||
preventCleanup: false,
|
||||
keepAllVersionsNewerThanDays: null,
|
||||
keepLatestVersionPerDayForDays: null,
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
id: 'block-alignment-id',
|
||||
container: { id: 'settings-group-key' },
|
||||
alias: 'blockAlignment',
|
||||
name: 'Block Alignment',
|
||||
description: '',
|
||||
dataType: { id: 'dt-dropdown-align' },
|
||||
variesByCulture: false,
|
||||
variesBySegment: false,
|
||||
sortOrder: 0,
|
||||
validation: {
|
||||
mandatory: false,
|
||||
mandatoryMessage: null,
|
||||
regEx: null,
|
||||
regExMessage: null,
|
||||
},
|
||||
appearance: {
|
||||
labelOnTop: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
id: 'settings-group-key',
|
||||
parent: null,
|
||||
name: 'Settings',
|
||||
type: 'Group',
|
||||
sortOrder: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
allowedTemplates: [],
|
||||
defaultTemplate: null,
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
|
||||
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UmbInputNumberRangeElement } from '@umbraco-cms/backoffice/components';
|
||||
|
||||
@customElement('umb-block-grid-area-type-workspace-view')
|
||||
export class UmbBlockGridAreaTypeWorkspaceViewSettingsElement extends UmbLitElement implements UmbWorkspaceViewElement {
|
||||
// TODO: Add Localizations...
|
||||
// TODO: Validation to prevent spaces and weird characters in alias:
|
||||
// TODO: Add create button label field:
|
||||
// TODO: Turn minAllowed and maxAllowed into one range property/input...
|
||||
// TODO: Add validation permission field:
|
||||
|
||||
#dataset?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
|
||||
|
||||
@state()
|
||||
_minValue?: number;
|
||||
@state()
|
||||
_maxValue?: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
|
||||
this.#dataset = context;
|
||||
this.observe(await this.#dataset.propertyValueByAlias<number>('minAllowed'), (min) => {
|
||||
this._minValue = min ?? 0;
|
||||
});
|
||||
this.observe(await this.#dataset.propertyValueByAlias<number>('maxAllowed'), (max) => {
|
||||
this._maxValue = max ?? Infinity;
|
||||
});
|
||||
});
|
||||
}
|
||||
#onAllowedRangeChange = (e: UmbChangeEvent) => {
|
||||
this.#dataset?.setPropertyValue('minAllowed', (e!.target! as UmbInputNumberRangeElement).minValue);
|
||||
this.#dataset?.setPropertyValue('maxAllowed', (e!.target! as UmbInputNumberRangeElement).maxValue);
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box headline=${'Identification'}>
|
||||
@@ -24,14 +49,14 @@ export class UmbBlockGridAreaTypeWorkspaceViewSettingsElement extends UmbLitElem
|
||||
property-editor-ui-alias="Umb.PropertyEditorUi.TextBox"></umb-property>
|
||||
</uui-box>
|
||||
<uui-box headline=${'Validation'}>
|
||||
<umb-property
|
||||
label=${'minAllowed'}
|
||||
alias="minAllowed"
|
||||
property-editor-ui-alias="Umb.PropertyEditorUi.TextBox"></umb-property>
|
||||
<umb-property
|
||||
label=${'maxAllowed'}
|
||||
alias="maxAllowed"
|
||||
property-editor-ui-alias="Umb.PropertyEditorUi.TextBox"></umb-property>
|
||||
<umb-property-layout label=${'rangeAllowed'}>
|
||||
<umb-input-number-range
|
||||
slot="editor"
|
||||
.minValue=${this._minValue}
|
||||
.maxValue=${this._maxValue}
|
||||
@change=${this.#onAllowedRangeChange}>
|
||||
</umb-input-number-range>
|
||||
</umb-property-layout>
|
||||
|
||||
<umb-property
|
||||
label=${'specifiedAllowance'}
|
||||
|
||||
@@ -134,11 +134,23 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
});
|
||||
|
||||
#context = new UmbBlockGridEntriesContext(this);
|
||||
#controlValidator?: UmbFormControlValidator;
|
||||
#typeLimitValidator?: UmbFormControlValidatorConfig;
|
||||
#rangeUnderflowValidator?: UmbFormControlValidatorConfig;
|
||||
#rangeOverflowValidator?: UmbFormControlValidatorConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ type: String, attribute: 'area-key', reflect: true })
|
||||
public set areaKey(value: string | null | undefined) {
|
||||
this._areaKey = value;
|
||||
this.#context.setAreaKey(value ?? null);
|
||||
this.#controlValidator?.destroy();
|
||||
if (this.areaKey) {
|
||||
// Only when there is a area key we should create a validator, otherwise it is the root entries element, which is taking part of the Property Editor Form Control. [NL]
|
||||
// Currently there is no server validation for areas. So we can leave out the data path for it for now. [NL]
|
||||
this.#controlValidator = new UmbFormControlValidator(this, this);
|
||||
|
||||
//new UmbBindServerValidationToFormControl(this, this, "$.values.[?(@.alias = 'my-input-alias')].value");
|
||||
}
|
||||
}
|
||||
public get areaKey(): string | null | undefined {
|
||||
return this._areaKey;
|
||||
@@ -169,6 +181,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.observe(
|
||||
this.#context.layoutEntries,
|
||||
(layoutEntries) => {
|
||||
@@ -207,6 +220,14 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
null,
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#context.hasTypeLimits,
|
||||
(hasTypeLimits) => {
|
||||
this.#setupBlockTypeLimitValidation(hasTypeLimits);
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
this.#context.getManager().then((manager) => {
|
||||
this.observe(
|
||||
manager.layoutStylesheet,
|
||||
@@ -223,8 +244,6 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
new UmbFormControlValidator(this, this /*, this.#dataPath*/);
|
||||
}
|
||||
|
||||
#rangeUnderflowValidator?: UmbFormControlValidatorConfig;
|
||||
#rangeOverflowValidator?: UmbFormControlValidatorConfig;
|
||||
async #setupRangeValidation(rangeLimit: UmbNumberRangeValueType | undefined) {
|
||||
if (this.#rangeUnderflowValidator) {
|
||||
this.removeValidator(this.#rangeUnderflowValidator);
|
||||
@@ -240,9 +259,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
(rangeLimit!.min ?? 0) - this._layoutEntries.length,
|
||||
);
|
||||
},
|
||||
() => {
|
||||
return this._layoutEntries.length < (rangeLimit?.min ?? 0);
|
||||
},
|
||||
() => this._layoutEntries.length < (rangeLimit?.min ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -260,8 +277,37 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
this._layoutEntries.length - (rangeLimit!.max ?? this._layoutEntries.length),
|
||||
);
|
||||
},
|
||||
() => this._layoutEntries.length > (rangeLimit?.max ?? Infinity),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async #setupBlockTypeLimitValidation(hasTypeLimits: boolean | undefined) {
|
||||
if (this.#typeLimitValidator) {
|
||||
this.removeValidator(this.#typeLimitValidator);
|
||||
this.#typeLimitValidator = undefined;
|
||||
}
|
||||
if (hasTypeLimits) {
|
||||
this.#typeLimitValidator = this.addValidator(
|
||||
'customError',
|
||||
() => {
|
||||
return (this._layoutEntries.length ?? 0) > (rangeLimit?.max ?? Infinity);
|
||||
const invalids = this.#context.getInvalidBlockTypeLimits();
|
||||
return invalids
|
||||
.map((invalidRule) =>
|
||||
this.localize.term(
|
||||
invalidRule.amount < invalidRule.minRequirement
|
||||
? 'blockEditor_areaValidationEntriesShort'
|
||||
: 'blockEditor_areaValidationEntriesExceed',
|
||||
invalidRule.name,
|
||||
invalidRule.amount,
|
||||
invalidRule.minRequirement,
|
||||
invalidRule.maxRequirement,
|
||||
),
|
||||
)
|
||||
.join(', ');
|
||||
},
|
||||
() => {
|
||||
return !this.#context.checkBlockTypeLimitsValidity();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -284,14 +330,14 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
</umb-block-grid-entry>`,
|
||||
)}
|
||||
</div>
|
||||
<uui-form-validation-message .for=${this}></uui-form-validation-message>
|
||||
${this._canCreate ? this.#renderCreateButton() : nothing}
|
||||
${this._areaKey ? html` <uui-form-validation-message .for=${this}></uui-form-validation-message>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderCreateButton() {
|
||||
if (this._areaKey === null || this._layoutEntries.length === 0) {
|
||||
return html`<uui-button-group>
|
||||
return html`<uui-button-group id="createButton">
|
||||
<uui-button
|
||||
look="placeholder"
|
||||
label=${this._singleBlockTypeName
|
||||
@@ -343,19 +389,31 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
uui-button-group {
|
||||
#createButton {
|
||||
padding-top: 1px;
|
||||
grid-template-columns: 1fr auto;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
// Only when we are n an area, we like to hide the button on drag
|
||||
:host([area-key]) #createButton {
|
||||
--umb-block-grid--is-dragging--variable: var(--umb-block-grid--is-dragging) none;
|
||||
display: var(--umb-block-grid--is-dragging--variable, grid);
|
||||
}
|
||||
:host(:not([pristine]):invalid) #createButton {
|
||||
--uui-button-contrast: var(--uui-color-danger);
|
||||
--uui-button-contrast-hover: var(--uui-color-danger);
|
||||
--uui-color-default-emphasis: var(--uui-color-danger);
|
||||
--uui-button-border-color: var(--uui-color-danger);
|
||||
--uui-button-border-color-hover: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
.umb-block-grid__layout-container[data-area-length='0'] {
|
||||
--umb-block-grid--is-dragging--variable: var(--umb-block-grid--is-dragging) 1;
|
||||
|
||||
@@ -13,6 +13,10 @@ import { UMB_BLOCK_GRID, type UmbBlockGridLayoutModel } from '@umbraco-cms/backo
|
||||
import '../block-grid-block-inline/index.js';
|
||||
import '../block-grid-block/index.js';
|
||||
import '../block-scale-handler/index.js';
|
||||
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
|
||||
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
|
||||
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
|
||||
/**
|
||||
* @element umb-block-grid-entry
|
||||
*/
|
||||
@@ -37,6 +41,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this._blockViewProps.contentUdi = value;
|
||||
this.setAttribute('data-element-udi', value);
|
||||
this.#context.setContentUdi(value);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
`$.contentData[${UmbDataPathBlockElementDataQuery({ udi: value })}]`,
|
||||
(hasMessages) => {
|
||||
this._contentInvalid = hasMessages;
|
||||
this._blockViewProps.contentInvalid = hasMessages;
|
||||
},
|
||||
'observeMessagesForContent',
|
||||
);
|
||||
}
|
||||
private _contentUdi?: string | undefined;
|
||||
//
|
||||
@@ -89,6 +103,14 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
@state()
|
||||
_inlineCreateAboveWidth?: string;
|
||||
|
||||
// 'content-invalid' attribute is used for styling purpose.
|
||||
@property({ type: Boolean, attribute: 'content-invalid', reflect: true })
|
||||
_contentInvalid?: boolean;
|
||||
|
||||
// 'settings-invalid' attribute is used for styling purpose.
|
||||
@property({ type: Boolean, attribute: 'settings-invalid', reflect: true })
|
||||
_settingsInvalid?: boolean;
|
||||
|
||||
@state()
|
||||
_blockViewProps: UmbBlockEditorCustomViewProperties<UmbBlockGridLayoutModel> = {
|
||||
contentUdi: undefined!,
|
||||
@@ -178,6 +200,20 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this.#context.settings,
|
||||
(settings) => {
|
||||
this.#updateBlockViewProps({ settings });
|
||||
|
||||
this.removeUmbControllerByAlias('observeMessagesForSettings');
|
||||
if (settings) {
|
||||
// Observe settings validation state:
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
`$.settingsData[${UmbDataPathBlockElementDataQuery(settings)}]`,
|
||||
(hasMessages) => {
|
||||
this._settingsInvalid = hasMessages;
|
||||
this._blockViewProps.settingsInvalid = hasMessages;
|
||||
},
|
||||
'observeMessagesForSettings',
|
||||
);
|
||||
}
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -318,12 +354,23 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
return true;
|
||||
};
|
||||
|
||||
#extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer<ManifestBlockEditorCustomView>) => {
|
||||
if (ext.component) {
|
||||
ext.component.classList.add('umb-block-grid__block--view');
|
||||
}
|
||||
return ext.component;
|
||||
};
|
||||
|
||||
#renderInlineEditBlock() {
|
||||
return html`<umb-block-grid-block-inline .label=${this._label}></umb-block-grid-block-inline>`;
|
||||
return html`<umb-block-grid-block-inline
|
||||
class="umb-block-grid__block--view"
|
||||
.label=${this._label}></umb-block-grid-block-inline>`;
|
||||
}
|
||||
|
||||
#renderRefBlock() {
|
||||
return html`<umb-block-grid-block .label=${this._label}></umb-block-grid-block>`;
|
||||
return html`<umb-block-grid-block
|
||||
class="umb-block-grid__block--view"
|
||||
.label=${this._label}></umb-block-grid-block>`;
|
||||
}
|
||||
|
||||
#renderBlock() {
|
||||
@@ -339,28 +386,47 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
: nothing}
|
||||
<div class="umb-block-grid__block" part="umb-block-grid__block">
|
||||
<umb-extension-slot
|
||||
type="blockEditorCustomView"
|
||||
default-element="umb-block-grid-block"
|
||||
.props=${this._blockViewProps}
|
||||
.filter=${this.#extensionSlotFilterMethod}
|
||||
.renderMethod=${this.#extensionSlotRenderMethod}
|
||||
.props=${this._blockViewProps}
|
||||
default-element=${this._inlineEditingMode ? 'umb-block-grid-block-inline' : 'umb-block-grid-block'}
|
||||
type="blockEditorCustomView"
|
||||
single
|
||||
>${this._inlineEditingMode ? this.#renderInlineEditBlock() : this.#renderRefBlock()}</umb-extension-slot
|
||||
>
|
||||
<uui-action-bar>
|
||||
${this._showContentEdit && this._workspaceEditContentPath
|
||||
? html`<uui-button label="edit" compact href=${this._workspaceEditContentPath}>
|
||||
? html`<uui-button
|
||||
label="edit"
|
||||
look="secondary"
|
||||
color=${this._contentInvalid ? 'danger' : ''}
|
||||
href=${this._workspaceEditContentPath}>
|
||||
<uui-icon name="icon-edit"></uui-icon>
|
||||
${this._contentInvalid
|
||||
? html`<uui-badge attention color="danger" label="Invalid content">!</uui-badge>`
|
||||
: nothing}
|
||||
</uui-button>`
|
||||
: nothing}
|
||||
${this._hasSettings && this._workspaceEditSettingsPath
|
||||
? html`<uui-button label="Edit settings" compact href=${this._workspaceEditSettingsPath}>
|
||||
? html`<uui-button
|
||||
label="Edit settings"
|
||||
look="secondary"
|
||||
color=${this._settingsInvalid ? 'danger' : ''}
|
||||
href=${this._workspaceEditSettingsPath}>
|
||||
<uui-icon name="icon-settings"></uui-icon>
|
||||
${this._settingsInvalid
|
||||
? html`<uui-badge attention color="danger" label="Invalid settings">!</uui-badge>`
|
||||
: nothing}
|
||||
</uui-button>`
|
||||
: nothing}
|
||||
<uui-button label="delete" compact @click=${() => this.#context.requestDelete()}>
|
||||
<uui-button label="delete" look="secondary" @click=${() => this.#context.requestDelete()}>
|
||||
<uui-icon name="icon-remove"></uui-icon>
|
||||
</uui-button>
|
||||
</uui-action-bar>
|
||||
|
||||
${!this._showContentEdit && this._contentInvalid
|
||||
? html`<uui-badge attention color="danger" label="Invalid content">!</uui-badge>`
|
||||
: nothing}
|
||||
${this._canScale
|
||||
? html` <umb-block-scale-handler
|
||||
@mousedown=${(e: MouseEvent) => this.#context.scaleManager.onScaleMouseDown(e)}>
|
||||
@@ -383,21 +449,31 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UUIBlinkKeyframes,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--umb-block-grid-entry-actions-opacity: 0;
|
||||
}
|
||||
:host([settings-invalid]),
|
||||
:host([content-invalid]),
|
||||
:host(:hover),
|
||||
:host(:focus-within) {
|
||||
--umb-block-grid-entry-actions-opacity: 1;
|
||||
}
|
||||
|
||||
uui-action-bar {
|
||||
position: absolute;
|
||||
top: var(--uui-size-2);
|
||||
right: var(--uui-size-2);
|
||||
opacity: var(--umb-block-grid-entry-actions-opacity, 0);
|
||||
transition: opacity 120ms;
|
||||
}
|
||||
uui-button-inline-create {
|
||||
top: 0px;
|
||||
position: absolute;
|
||||
|
||||
// Avoid showing inline-create in dragging-mode
|
||||
--umb-block-grid__block--inline-create-button-display--condition: var(--umb-block-grid--dragging-mode) none;
|
||||
display: var(--umb-block-grid__block--inline-create-button-display--condition);
|
||||
}
|
||||
@@ -412,35 +488,60 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
right: calc(1px - (var(--umb-block-grid--column-gap, 0px) * 0.5));
|
||||
}
|
||||
|
||||
:host([drag-placeholder]) {
|
||||
opacity: 0.2;
|
||||
.umb-block-grid__block {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host(::after) {
|
||||
:host::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
inset: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
border-radius: var(--uui-border-radius);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.7),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.7);
|
||||
|
||||
transition: border-color 240ms ease-in;
|
||||
}
|
||||
|
||||
:host(:hover::after) {
|
||||
// TODO: Look at the feature I out-commented here, what was that suppose to do [NL]:
|
||||
//display: var(--umb-block-grid--block-ui-display, block);
|
||||
:host(:hover):not(:drop)::after {
|
||||
display: block;
|
||||
border-color: var(--uui-color-interactive);
|
||||
border-color: var(--uui-color-interactive-emphasis);
|
||||
}
|
||||
|
||||
.umb-block-grid__block {
|
||||
height: 100%;
|
||||
:host([drag-placeholder])::after {
|
||||
display: block;
|
||||
border-width: 2px;
|
||||
border-color: var(--uui-color-interactive-emphasis);
|
||||
animation: ${UUIBlinkAnimationValue};
|
||||
}
|
||||
:host([drag-placeholder])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
inset: 0;
|
||||
border-radius: var(--uui-border-radius);
|
||||
background-color: var(--uui-color-interactive-emphasis);
|
||||
opacity: 0.12;
|
||||
}
|
||||
:host([drag-placeholder]) .umb-block-grid__block {
|
||||
transition: opacity 50ms 16ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host([settings-invalid])::after,
|
||||
:host([content-invalid])::after {
|
||||
border-color: var(--uui-color-danger);
|
||||
}
|
||||
:host([settings-invalid])::before,
|
||||
:host([content-invalid])::before {
|
||||
background-color: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
uui-badge {
|
||||
z-index: 2;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -8,7 +8,13 @@ import {
|
||||
import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel } from '../types.js';
|
||||
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js';
|
||||
import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js';
|
||||
import { UmbArrayState, UmbNumberState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import {
|
||||
UmbArrayState,
|
||||
UmbBooleanState,
|
||||
UmbNumberState,
|
||||
UmbObjectState,
|
||||
UmbStringState,
|
||||
} from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
|
||||
import { pathFolderName } from '@umbraco-cms/backoffice/utils';
|
||||
@@ -49,6 +55,9 @@ export class UmbBlockGridEntriesContext
|
||||
public readonly amountOfAllowedBlockTypes = this.#allowedBlockTypes.asObservablePart((x) => x.length);
|
||||
public readonly canCreate = this.#allowedBlockTypes.asObservablePart((x) => x.length > 0);
|
||||
|
||||
#hasTypeLimits = new UmbBooleanState(undefined);
|
||||
public readonly hasTypeLimits = this.#hasTypeLimits.asObservable();
|
||||
|
||||
firstAllowedBlockTypeName() {
|
||||
if (!this._manager) {
|
||||
throw new Error('Manager not ready');
|
||||
@@ -84,6 +93,10 @@ export class UmbBlockGridEntriesContext
|
||||
this.#workspaceModal.setUniquePathValue('areaKey', areaKey ?? 'null');
|
||||
this.#catalogueModal.setUniquePathValue('areaKey', areaKey ?? 'null');
|
||||
this.#gotAreaKey();
|
||||
|
||||
// Idea: If we need to parse down a validation data path to target the specific layout object: [NL]
|
||||
// If we have a areaKey, we want to inherit our layoutDataPath from nearest blockGridEntry context.
|
||||
// If not, we want to set the layoutDataPath to a base one.
|
||||
}
|
||||
|
||||
setLayoutColumns(columns: number | undefined) {
|
||||
@@ -133,9 +146,30 @@ export class UmbBlockGridEntriesContext
|
||||
blockGroups: this._manager?.getBlockGroups() ?? [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
originData: { index: index, areaKey: this.#areaKey, parentUnique: this.#parentUnique },
|
||||
createBlockInWorkspace: true,
|
||||
},
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
// We can parse an empty object, cause the rest will be filled in by others.
|
||||
{} as any,
|
||||
data.originData as UmbBlockGridWorkspaceOriginData,
|
||||
);
|
||||
if (created) {
|
||||
this.insert(
|
||||
created.layout,
|
||||
created.content,
|
||||
created.settings,
|
||||
data.originData as UmbBlockGridWorkspaceOriginData,
|
||||
);
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
}
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
// TODO: Does it make any sense that this is a state? Check usage and confirm. [NL]
|
||||
this._catalogueRouteBuilderState.setValue(routeBuilder);
|
||||
@@ -163,8 +197,8 @@ export class UmbBlockGridEntriesContext
|
||||
protected _gotBlockManager() {
|
||||
if (!this._manager) return;
|
||||
|
||||
this.#getAllowedBlockTypes();
|
||||
this.#getRangeLimits();
|
||||
this.#setupAllowedBlockTypes();
|
||||
this.#setupRangeLimits();
|
||||
|
||||
this.observe(
|
||||
this._manager.propertyAlias,
|
||||
@@ -216,8 +250,6 @@ export class UmbBlockGridEntriesContext
|
||||
'observeThisLayouts',
|
||||
);
|
||||
|
||||
this.removeUmbControllerByAlias('observeAreaType');
|
||||
|
||||
const hostEl = this.getHostElement() as HTMLElement | undefined;
|
||||
if (hostEl) {
|
||||
hostEl.removeAttribute('data-area-alias');
|
||||
@@ -229,8 +261,8 @@ export class UmbBlockGridEntriesContext
|
||||
}
|
||||
|
||||
this.removeUmbControllerByAlias('observeAreaType');
|
||||
this.#getAllowedBlockTypes();
|
||||
this.#getRangeLimits();
|
||||
this.#setupAllowedBlockTypes();
|
||||
this.#setupRangeLimits();
|
||||
} else {
|
||||
if (!this.#parentEntry) return;
|
||||
|
||||
@@ -273,22 +305,44 @@ export class UmbBlockGridEntriesContext
|
||||
hostEl.style.setProperty('--umb-block-grid--grid-columns', areaType?.columnSpan?.toString() ?? '');
|
||||
hostEl.style.setProperty('--umb-block-grid--area-column-span', areaType?.columnSpan?.toString() ?? '');
|
||||
hostEl.style.setProperty('--umb-block-grid--area-row-span', areaType?.rowSpan?.toString() ?? '');
|
||||
this.#getAllowedBlockTypes();
|
||||
this.#getRangeLimits();
|
||||
this.#setupAllowedBlockTypes();
|
||||
this.#setupRangeLimits();
|
||||
},
|
||||
'observeAreaType',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#getAllowedBlockTypes() {
|
||||
#setupAllowedBlockTypes() {
|
||||
if (!this._manager) return;
|
||||
this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes());
|
||||
this.#setupAllowedBlockTypesLimits();
|
||||
}
|
||||
#getRangeLimits() {
|
||||
#setupRangeLimits() {
|
||||
if (!this._manager) return;
|
||||
const range = this.#retrieveRangeLimits();
|
||||
this.#rangeLimits.setValue(range);
|
||||
//const range = this.#retrieveRangeLimits();
|
||||
if (this.#areaKey != null) {
|
||||
this.removeUmbControllerByAlias('observeConfigurationRootLimits');
|
||||
// Area entries:
|
||||
if (!this.#areaType) return undefined;
|
||||
// No need to observe as this method is called every time the area is changed.
|
||||
this.#rangeLimits.setValue({
|
||||
min: this.#areaType.minAllowed ?? 0,
|
||||
max: this.#areaType.maxAllowed ?? Infinity,
|
||||
});
|
||||
} else if (this.#areaKey === null) {
|
||||
if (!this._manager) return undefined;
|
||||
|
||||
this.observe(
|
||||
this._manager.editorConfiguration,
|
||||
(config) => {
|
||||
const min = config?.getValueByAlias<UmbNumberRangeValueType>('validationLimit')?.min ?? 0;
|
||||
const max = config?.getValueByAlias<UmbNumberRangeValueType>('validationLimit')?.max ?? Infinity;
|
||||
this.#rangeLimits.setValue({ min, max });
|
||||
},
|
||||
'observeConfigurationRootLimits',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getPathForCreateBlock(index: number) {
|
||||
@@ -385,24 +439,95 @@ export class UmbBlockGridEntriesContext
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @returns an NumberRange of the min and max allowed items in the current area. Or undefined if not ready jet.
|
||||
*/
|
||||
#retrieveRangeLimits(): UmbNumberRangeValueType | undefined {
|
||||
if (this.#areaKey != null) {
|
||||
#setupAllowedBlockTypesLimits() {
|
||||
if (!this._manager) return;
|
||||
|
||||
if (this.#areaKey) {
|
||||
// Area entries:
|
||||
if (!this.#areaType) return undefined;
|
||||
if (!this.#areaType) return;
|
||||
|
||||
return { min: this.#areaType.minAllowed ?? 0, max: this.#areaType.maxAllowed ?? Infinity };
|
||||
if (this.#areaType.specifiedAllowance && this.#areaType.specifiedAllowance?.length > 0) {
|
||||
this.#hasTypeLimits.setValue(true);
|
||||
}
|
||||
} else if (this.#areaKey === null) {
|
||||
if (!this._manager) return undefined;
|
||||
|
||||
const config = this._manager.getEditorConfiguration();
|
||||
const min = config?.getValueByAlias<UmbNumberRangeValueType>('validationLimit')?.min ?? 0;
|
||||
const max = config?.getValueByAlias<UmbNumberRangeValueType>('validationLimit')?.max ?? Infinity;
|
||||
return { min, max };
|
||||
// RESET
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
#invalidBlockTypeLimits?: Array<{
|
||||
groupKey?: string;
|
||||
key?: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
minRequirement: number;
|
||||
maxRequirement: number;
|
||||
}>;
|
||||
|
||||
getInvalidBlockTypeLimits() {
|
||||
return this.#invalidBlockTypeLimits ?? [];
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
* @returns {boolean} - True if the block type limits are valid, otherwise false.
|
||||
*/
|
||||
checkBlockTypeLimitsValidity(): boolean {
|
||||
if (!this.#areaType || !this.#areaType.specifiedAllowance) return false;
|
||||
|
||||
const layoutEntries = this._layoutEntries.getValue();
|
||||
|
||||
this.#invalidBlockTypeLimits = [];
|
||||
|
||||
const hasInvalidRules = this.#areaType.specifiedAllowance.some((rule) => {
|
||||
const minAllowed = rule.minAllowed || 0;
|
||||
const maxAllowed = rule.maxAllowed || 0;
|
||||
|
||||
// For block groups:
|
||||
if (rule.groupKey) {
|
||||
const groupElementTypeKeys =
|
||||
this._manager
|
||||
?.getBlockTypes()
|
||||
.filter((blockType) => blockType.groupKey === rule.groupKey && blockType.allowInAreas === true)
|
||||
.map((x) => x.contentElementTypeKey) ?? [];
|
||||
const groupAmount = layoutEntries.filter((entry) => {
|
||||
const contentTypeKey = this._manager!.getContentTypeKeyOf(entry.contentUdi);
|
||||
return contentTypeKey ? groupElementTypeKeys.indexOf(contentTypeKey) !== -1 : false;
|
||||
}).length;
|
||||
|
||||
if (groupAmount < minAllowed || (maxAllowed > 0 && groupAmount > maxAllowed)) {
|
||||
this.#invalidBlockTypeLimits!.push({
|
||||
groupKey: rule.groupKey,
|
||||
name: this._manager!.getBlockGroupName(rule.groupKey) ?? '?',
|
||||
amount: groupAmount,
|
||||
minRequirement: minAllowed,
|
||||
maxRequirement: maxAllowed,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// For specific elementTypes:
|
||||
else if (rule.elementTypeKey) {
|
||||
const amount = layoutEntries.filter((entry) => {
|
||||
const contentTypeKey = this._manager!.getContentOf(entry.contentUdi)?.contentTypeKey;
|
||||
return contentTypeKey === rule.elementTypeKey;
|
||||
}).length;
|
||||
if (amount < minAllowed || (maxAllowed > 0 ? amount > maxAllowed : false)) {
|
||||
this.#invalidBlockTypeLimits!.push({
|
||||
key: rule.elementTypeKey,
|
||||
name: this._manager!.getContentTypeNameOf(rule.elementTypeKey) ?? '?',
|
||||
amount: amount,
|
||||
minRequirement: minAllowed,
|
||||
maxRequirement: maxAllowed,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Lets fail cause the rule was bad.
|
||||
console.error('Invalid block type limit rule.', rule);
|
||||
return false;
|
||||
});
|
||||
return hasInvalidRules === false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,6 +62,9 @@ export class UmbBlockGridManagerContext<
|
||||
getBlockGroups() {
|
||||
return this.#blockGroups.value;
|
||||
}
|
||||
getBlockGroupName(unique: string) {
|
||||
return this.#blockGroups.getValue().find((group) => group.key === unique)?.name;
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { UmbBlockGridManagerContext } from '../../context/block-grid-manager.context.js';
|
||||
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_ALIAS } from './manifests.js';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { html, customElement, property, state, css, type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
css,
|
||||
type PropertyValueMap,
|
||||
ref,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
||||
import '../../components/block-grid-entries/index.js';
|
||||
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
|
||||
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
import { UmbBlockGridManagerContext } from '../../context/block-grid-manager.context.js';
|
||||
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_ALIAS } from './manifests.js';
|
||||
import { UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
|
||||
import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type';
|
||||
import type { UmbBlockGridTypeModel, UmbBlockGridValueModel } from '@umbraco-cms/backoffice/block-grid';
|
||||
import { UmbBlockElementDataValidationPathTranslator } from '@umbraco-cms/backoffice/block';
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-block-grid
|
||||
@@ -20,6 +29,9 @@ export class UmbPropertyEditorUIBlockGridElement
|
||||
extends UmbFormControlMixin<UmbBlockGridValueModel, typeof UmbLitElement>(UmbLitElement)
|
||||
implements UmbPropertyEditorUiElement
|
||||
{
|
||||
#validationContext = new UmbValidationContext(this).provide();
|
||||
#contentDataPathTranslator?: UmbBlockElementDataValidationPathTranslator;
|
||||
#settingsDataPathTranslator?: UmbBlockElementDataValidationPathTranslator;
|
||||
#context = new UmbBlockGridManagerContext(this);
|
||||
//
|
||||
private _value: UmbBlockGridValueModel = {
|
||||
@@ -31,11 +43,6 @@ export class UmbPropertyEditorUIBlockGridElement
|
||||
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
|
||||
if (!config) return;
|
||||
|
||||
/*const validationLimit = config.getValueByAlias<NumberRangeValueType>('validationLimit');
|
||||
|
||||
this.#limitMin = validationLimit?.min;
|
||||
this.#limitMax = validationLimit?.max;*/
|
||||
|
||||
const blocks = config.getValueByAlias<Array<UmbBlockGridTypeModel>>('blocks') ?? [];
|
||||
this.#context.setBlockTypes(blocks);
|
||||
|
||||
@@ -44,7 +51,7 @@ export class UmbPropertyEditorUIBlockGridElement
|
||||
|
||||
this.style.maxWidth = config.getValueByAlias<string>('maxPropertyWidth') ?? '';
|
||||
|
||||
//config.useLiveEditing, is covered by the EditorConfiguration of context.
|
||||
//config.useLiveEditing, is covered by the EditorConfiguration of context. [NL]
|
||||
this.#context.setEditorConfiguration(config);
|
||||
}
|
||||
|
||||
@@ -70,6 +77,25 @@ export class UmbPropertyEditorUIBlockGridElement
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
context.dataPath,
|
||||
(dataPath) => {
|
||||
// Translate paths for content/settings:
|
||||
this.#contentDataPathTranslator?.destroy();
|
||||
this.#settingsDataPathTranslator?.destroy();
|
||||
if (dataPath) {
|
||||
// Set the data path for the local validation context:
|
||||
this.#validationContext.setDataPath(dataPath);
|
||||
|
||||
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
|
||||
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
|
||||
}
|
||||
},
|
||||
'observeDataPath',
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Prevent initial notification from these observes
|
||||
this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => {
|
||||
this.observe(
|
||||
@@ -99,8 +125,21 @@ export class UmbPropertyEditorUIBlockGridElement
|
||||
});
|
||||
}
|
||||
|
||||
#currentEntriesElement?: Element;
|
||||
#gotRootEntriesElement(element: Element | undefined): void {
|
||||
if (this.#currentEntriesElement === element) return;
|
||||
if (this.#currentEntriesElement) {
|
||||
this.removeFormControlElement(this.#currentEntriesElement as any);
|
||||
}
|
||||
this.#currentEntriesElement = element;
|
||||
if (element) {
|
||||
this.addFormControlElement(element as any);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` <umb-block-grid-entries
|
||||
${ref(this.#gotRootEntriesElement)}
|
||||
.areaKey=${null}
|
||||
.layoutColumns=${this._layoutColumns}></umb-block-grid-entries>`;
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
default-element=${this._inlineEditingMode ? 'umb-inline-list-block' : 'umb-ref-list-block'}
|
||||
.props=${this._blockViewProps}
|
||||
.filter=${this.#extensionSlotFilterMethod}
|
||||
single
|
||||
>${this._inlineEditingMode ? this.#renderInlineBlock() : this.#renderRefBlock()}</umb-extension-slot
|
||||
>
|
||||
<uui-action-bar>
|
||||
|
||||
@@ -139,21 +139,14 @@ export class UmbPropertyEditorUIBlockListElement
|
||||
this.observe(
|
||||
context.dataPath,
|
||||
(dataPath) => {
|
||||
// Translate paths for content elements:
|
||||
// Translate paths for content/settings:
|
||||
this.#contentDataPathTranslator?.destroy();
|
||||
if (dataPath) {
|
||||
// Set the data path for the local validation context:
|
||||
this.#validationContext.setDataPath(dataPath);
|
||||
|
||||
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
|
||||
}
|
||||
|
||||
// Translate paths for settings elements:
|
||||
this.#settingsDataPathTranslator?.destroy();
|
||||
if (dataPath) {
|
||||
// Set the data path for the local validation context:
|
||||
this.#validationContext.setDataPath(dataPath);
|
||||
|
||||
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
|
||||
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
|
||||
}
|
||||
},
|
||||
@@ -163,13 +156,13 @@ export class UmbPropertyEditorUIBlockListElement
|
||||
|
||||
this.addValidator(
|
||||
'rangeUnderflow',
|
||||
() => this.localize.term('validation_entriesShort'),
|
||||
() => '#validation_entriesShort',
|
||||
() => !!this._limitMin && this.#entriesContext.getLength() < this._limitMin,
|
||||
);
|
||||
|
||||
this.addValidator(
|
||||
'rangeOverflow',
|
||||
() => this.localize.term('validation_entriesExceed'),
|
||||
() => '#validation_entriesExceed',
|
||||
() => !!this._limitMax && this.#entriesContext.getLength() > this._limitMax,
|
||||
);
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
type="blockEditorCustomView"
|
||||
default-element=${'umb-ref-rte-block'}
|
||||
.props=${this._blockViewProps}
|
||||
single
|
||||
>${this.#renderRefBlock()}</umb-extension-slot
|
||||
>
|
||||
<uui-action-bar>
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
|
||||
import {
|
||||
UMB_BLOCK_RTE_WORKSPACE_MODAL,
|
||||
type UmbBlockRteWorkspaceOriginData,
|
||||
type UmbBlockRteWorkspaceData,
|
||||
} from '../workspace/block-rte-workspace.modal-token.js';
|
||||
import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context-token.js';
|
||||
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
|
||||
@@ -41,9 +40,30 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
blockGroups: [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
originData: {},
|
||||
createBlockInWorkspace: true,
|
||||
},
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
// We can parse an empty object, cause the rest will be filled in by others.
|
||||
{} as any,
|
||||
data.originData as UmbBlockRteWorkspaceOriginData,
|
||||
);
|
||||
if (created) {
|
||||
this.insert(
|
||||
created.layout,
|
||||
created.content,
|
||||
created.settings,
|
||||
data.originData as UmbBlockRteWorkspaceOriginData,
|
||||
);
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
}
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this._catalogueRouteBuilderState.setValue(routeBuilder);
|
||||
});
|
||||
@@ -114,10 +134,10 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
async create(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<UmbBlockRteLayoutModel, 'contentUdi'>,
|
||||
modalData?: UmbBlockRteWorkspaceData,
|
||||
originData?: UmbBlockRteWorkspaceOriginData,
|
||||
) {
|
||||
await this._retrieveManager;
|
||||
return this._manager?.create(contentElementTypeKey, partialLayoutEntry, modalData);
|
||||
return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData);
|
||||
}
|
||||
|
||||
// insert Block?
|
||||
@@ -126,10 +146,10 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
layoutEntry: UmbBlockRteLayoutModel,
|
||||
content: UmbBlockDataType,
|
||||
settings: UmbBlockDataType | undefined,
|
||||
modalData: UmbBlockRteWorkspaceData,
|
||||
originData: UmbBlockRteWorkspaceOriginData,
|
||||
) {
|
||||
await this._retrieveManager;
|
||||
return this._manager?.insert(layoutEntry, content, settings, modalData) ?? false;
|
||||
return this._manager?.insert(layoutEntry, content, settings, originData) ?? false;
|
||||
}
|
||||
|
||||
// create Block?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
|
||||
import type { UmbBlockRteWorkspaceData } from '../index.js';
|
||||
import type { UmbBlockRteWorkspaceOriginData } from '../index.js';
|
||||
import type { UmbBlockDataType } from '../../block/types.js';
|
||||
import type { Editor } from '@umbraco-cms/backoffice/external/tinymce';
|
||||
import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
|
||||
@@ -36,7 +36,7 @@ export class UmbBlockRteManagerContext<
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentUdi'>,
|
||||
// This property is used by some implementations, but not used in this.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
originData?: UmbBlockRteWorkspaceData,
|
||||
originData?: UmbBlockRteWorkspaceOriginData,
|
||||
) {
|
||||
const data = super.createBlockData(contentElementTypeKey, partialLayoutEntry);
|
||||
|
||||
@@ -57,13 +57,13 @@ export class UmbBlockRteManagerContext<
|
||||
layoutEntry: BlockLayoutType,
|
||||
content: UmbBlockDataType,
|
||||
settings: UmbBlockDataType | undefined,
|
||||
modalData: UmbBlockRteWorkspaceData,
|
||||
originData: UmbBlockRteWorkspaceOriginData,
|
||||
) {
|
||||
if (!this.#editor) return false;
|
||||
|
||||
this._layouts.appendOne(layoutEntry);
|
||||
|
||||
this.insertBlockData(layoutEntry, content, settings, modalData);
|
||||
this.insertBlockData(layoutEntry, content, settings, originData);
|
||||
|
||||
if (layoutEntry.displayInline) {
|
||||
this.#editor.selection.setContent(
|
||||
|
||||
@@ -21,7 +21,7 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
|
||||
(x) => x.unique,
|
||||
);
|
||||
|
||||
@property({ type: String, attribute: false })
|
||||
@property({ type: String })
|
||||
href?: string;
|
||||
|
||||
@property({ type: String, attribute: false })
|
||||
|
||||
@@ -234,11 +234,11 @@ export abstract class UmbBlockEntryContext<
|
||||
}
|
||||
|
||||
#updateCreatePaths() {
|
||||
const index = this.#index.value;
|
||||
if (this._entries && index !== undefined) {
|
||||
if (this._entries) {
|
||||
this.observe(
|
||||
observeMultiple([this._entries.catalogueRouteBuilder, this._entries.canCreate]),
|
||||
([catalogueRouteBuilder, canCreate]) => {
|
||||
observeMultiple([this.index, this._entries.catalogueRouteBuilder, this._entries.canCreate]),
|
||||
([index, catalogueRouteBuilder, canCreate]) => {
|
||||
if (index === undefined) return;
|
||||
if (catalogueRouteBuilder && canCreate) {
|
||||
this.#createBeforePath.setValue(this._entries!.getPathForCreateBlock(index));
|
||||
this.#createAfterPath.setValue(this._entries!.getPathForCreateBlock(index + 1));
|
||||
|
||||
@@ -148,6 +148,13 @@ export abstract class UmbBlockManagerContext<
|
||||
getContentTypeNameOf(contentTypeKey: string) {
|
||||
return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.name;
|
||||
}
|
||||
getContentTypeKeyOf(contentTypeKey: string) {
|
||||
return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.unique;
|
||||
}
|
||||
getContentTypeHasProperties(contentTypeKey: string) {
|
||||
const properties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.properties;
|
||||
return properties ? properties.length > 0 : false;
|
||||
}
|
||||
blockTypeOf(contentTypeKey: string) {
|
||||
return this.#blockTypes.asObservablePart((source) =>
|
||||
source.find((x) => x.contentElementTypeKey === contentTypeKey),
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { UMB_BLOCK_WORKSPACE_MODAL } from '../../workspace/index.js';
|
||||
import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type';
|
||||
import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue } from '@umbraco-cms/backoffice/block';
|
||||
import {
|
||||
UMB_BLOCK_MANAGER_CONTEXT,
|
||||
type UmbBlockCatalogueModalData,
|
||||
type UmbBlockCatalogueModalValue,
|
||||
} from '@umbraco-cms/backoffice/block';
|
||||
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UMB_MODAL_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
@@ -14,8 +18,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
UmbBlockCatalogueModalData,
|
||||
UmbBlockCatalogueModalValue
|
||||
> {
|
||||
//
|
||||
private _search = '';
|
||||
#search = '';
|
||||
|
||||
private _groupedBlocks: Array<{ name?: string; blocks: Array<UmbBlockTypeWithGroupKey> }> = [];
|
||||
|
||||
@@ -28,6 +31,9 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
@state()
|
||||
private _filtered: Array<{ name?: string; blocks: Array<UmbBlockTypeWithGroupKey> }> = [];
|
||||
|
||||
@state()
|
||||
_manager?: typeof UMB_BLOCK_MANAGER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -49,6 +55,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_BLOCK_MANAGER_CONTEXT, (manager) => {
|
||||
this._manager = manager;
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -71,10 +81,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
}
|
||||
|
||||
#updateFiltered() {
|
||||
if (this._search.length === 0) {
|
||||
if (this.#search.length === 0) {
|
||||
this._filtered = this._groupedBlocks;
|
||||
} else {
|
||||
const search = this._search.toLowerCase();
|
||||
const search = this.#search.toLowerCase();
|
||||
this._filtered = this._groupedBlocks.map((group) => {
|
||||
return { ...group, blocks: group.blocks.filter((block) => block.label?.toLocaleLowerCase().includes(search)) };
|
||||
});
|
||||
@@ -82,7 +92,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
}
|
||||
|
||||
#onSearch(e: UUIInputEvent) {
|
||||
this._search = e.target.value as string;
|
||||
this.#search = e.target.value as string;
|
||||
this.#updateFiltered();
|
||||
}
|
||||
|
||||
@@ -98,7 +108,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
override render() {
|
||||
return html`
|
||||
<umb-body-layout headline="${this.localize.term('blockEditor_addBlock')}">
|
||||
${this.#renderViews()} ${this._openClipboard ? this.#renderClipboard() : this.#renderCreateEmpty()}
|
||||
${this.#renderViews()}${this.#renderMain()}
|
||||
<div slot="actions">
|
||||
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
|
||||
<uui-button
|
||||
@@ -111,6 +121,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
`;
|
||||
}
|
||||
|
||||
#renderMain() {
|
||||
return this._manager ? (this._openClipboard ? this.#renderClipboard() : this.#renderCreateEmpty()) : nothing;
|
||||
}
|
||||
|
||||
#renderClipboard() {
|
||||
return html`Clipboard`;
|
||||
}
|
||||
@@ -140,7 +154,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
.backgroundColor=${block.backgroundColor}
|
||||
.contentElementTypeKey=${block.contentElementTypeKey}
|
||||
@open=${() => this.#chooseBlock(block.contentElementTypeKey)}
|
||||
?href=${this._workspacePath
|
||||
.href=${this._workspacePath && this._manager!.getContentTypeHasProperties(block.contentElementTypeKey)
|
||||
? `${this._workspacePath}create/${block.contentElementTypeKey}`
|
||||
: undefined}>
|
||||
</umb-block-type-card>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { UMB_BLOCK_WORKSPACE_CONTEXT } from './index.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { customElement, css, html, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { customElement, css, html, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
@customElement('umb-block-workspace-editor')
|
||||
export class UmbBlockWorkspaceEditorElement extends UmbLitElement {
|
||||
@@ -8,10 +10,29 @@ export class UmbBlockWorkspaceEditorElement extends UmbLitElement {
|
||||
@property({ type: String, attribute: false })
|
||||
workspaceAlias?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
observeMultiple([
|
||||
context.isNew,
|
||||
context.content.structure.ownerContentTypePart((contentType) => contentType?.name),
|
||||
]),
|
||||
([isNew, name]) => {
|
||||
this._headline = this.localize.term(isNew ? 'general_add' : 'general_edit') + ' ' + name;
|
||||
},
|
||||
'observeOwnerContentElementTypeName',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@state()
|
||||
_headline: string = '';
|
||||
|
||||
override render() {
|
||||
return this.workspaceAlias
|
||||
? html` <umb-workspace-editor alias=${this.workspaceAlias} headline=${'BLOCK EDITOR'}> </umb-workspace-editor> `
|
||||
: '';
|
||||
? html` <umb-workspace-editor alias=${this.workspaceAlias} headline=${this._headline}> </umb-workspace-editor> `
|
||||
: nothing;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
import { UmbClassState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_MODAL_CONTEXT, type UmbModalContext } from '@umbraco-cms/backoffice/modal';
|
||||
import { decodeFilePath } from '@umbraco-cms/backoffice/utils';
|
||||
import {
|
||||
UMB_BLOCK_ENTRIES_CONTEXT,
|
||||
UMB_BLOCK_MANAGER_CONTEXT,
|
||||
type UmbBlockWorkspaceOriginData,
|
||||
type UmbBlockWorkspaceData,
|
||||
} from '@umbraco-cms/backoffice/block';
|
||||
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
|
||||
@@ -32,7 +33,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
#retrieveBlockManager;
|
||||
#blockEntries?: typeof UMB_BLOCK_ENTRIES_CONTEXT.TYPE;
|
||||
#retrieveBlockEntries;
|
||||
#modalContext?: typeof UMB_MODAL_CONTEXT.TYPE;
|
||||
#modalContext?: UmbModalContext<UmbBlockWorkspaceData>;
|
||||
#retrieveModalContext;
|
||||
|
||||
#entityType: string;
|
||||
@@ -68,7 +69,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
this.addValidationContext(this.settings.validation);
|
||||
|
||||
this.#retrieveModalContext = this.consumeContext(UMB_MODAL_CONTEXT, (context) => {
|
||||
this.#modalContext = context;
|
||||
this.#modalContext = context as any;
|
||||
context.onSubmit().catch(this.#modalRejected);
|
||||
}).asPromise();
|
||||
|
||||
@@ -180,7 +181,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
const blockCreated = await this.#blockEntries.create(
|
||||
contentElementTypeId,
|
||||
{},
|
||||
this.#modalContext.data as UmbBlockWorkspaceData,
|
||||
this.#modalContext.data.originData as UmbBlockWorkspaceOriginData,
|
||||
);
|
||||
if (!blockCreated) {
|
||||
throw new Error('Block Entries could not create block');
|
||||
@@ -200,7 +201,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
blockCreated.layout,
|
||||
blockCreated.content,
|
||||
blockCreated.settings,
|
||||
this.#modalContext.data as UmbBlockWorkspaceData,
|
||||
this.#modalContext.data.originData as UmbBlockWorkspaceOriginData,
|
||||
);
|
||||
if (!blockInserted) {
|
||||
throw new Error('Block Entries could not insert block');
|
||||
@@ -360,7 +361,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
layoutData,
|
||||
contentData,
|
||||
settingsData,
|
||||
this.#modalContext.data as UmbBlockWorkspaceData,
|
||||
this.#modalContext.data.originData as UmbBlockWorkspaceOriginData,
|
||||
);
|
||||
if (!blockInserted) {
|
||||
throw new Error('Block Entries could not insert block');
|
||||
@@ -368,7 +369,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
} else {
|
||||
// Update data:
|
||||
|
||||
this.#blockManager.setOneLayout(layoutData, this.#modalContext.data as UmbBlockWorkspaceData);
|
||||
this.#blockManager.setOneLayout(layoutData, this.#modalContext.data.originData as UmbBlockWorkspaceOriginData);
|
||||
if (contentData) {
|
||||
this.#blockManager.setOneContent(contentData);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
|
||||
override render() {
|
||||
//TODO: Using native input=color element instead of uui-color-picker due to its huge size and bad adaptability as a pop up
|
||||
return html`
|
||||
<uui-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<div id="item">
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-navigation"></uui-icon>`}
|
||||
<div class="color-wrapper">
|
||||
@@ -183,7 +183,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U
|
||||
return html`
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-navigation" class="handle"></uui-icon>`}
|
||||
|
||||
<uui-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<uui-input
|
||||
id="input"
|
||||
label="Value"
|
||||
@@ -90,7 +90,7 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U
|
||||
?readonly=${this.readonly}
|
||||
required=${this.required}
|
||||
required-message="Value is missing"></uui-input>
|
||||
</uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
|
||||
${when(
|
||||
!this.readonly,
|
||||
|
||||
@@ -15,16 +15,20 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
* @augments {UmbLitElement}
|
||||
*/
|
||||
|
||||
// TODO: Refactor extension-slot and extension-with-api slot.
|
||||
// TODO: Fire change event.
|
||||
// TODO: Make property that reveals the amount of displayed/permitted extensions.
|
||||
@customElement('umb-extension-slot')
|
||||
export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
#attached = false;
|
||||
#extensionsController?: UmbExtensionsElementInitializer;
|
||||
#extensionsController?: UmbExtensionsElementInitializer | UmbExtensionElementInitializer;
|
||||
|
||||
@state()
|
||||
private _permitted?: Array<UmbExtensionElementInitializer>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
single?: boolean;
|
||||
|
||||
/**
|
||||
* The type or types of extensions to render.
|
||||
* @type {string | string[]}
|
||||
@@ -77,7 +81,6 @@ export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
return this.#props;
|
||||
}
|
||||
set props(newVal: Record<string, unknown> | undefined) {
|
||||
// TODO, compare changes since last time. only reset the ones that changed. This might be better done by the controller is self:
|
||||
this.#props = newVal;
|
||||
if (this.#extensionsController) {
|
||||
this.#extensionsController.properties = newVal;
|
||||
@@ -88,7 +91,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
@property({ type: String, attribute: 'default-element' })
|
||||
public defaultElement?: string;
|
||||
|
||||
@property()
|
||||
@property({ attribute: false })
|
||||
public renderMethod?: (
|
||||
extension: UmbExtensionElementInitializer,
|
||||
index: number,
|
||||
@@ -128,15 +131,17 @@ export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
override render() {
|
||||
return this._permitted
|
||||
? this._permitted.length > 0
|
||||
? repeat(
|
||||
this._permitted,
|
||||
(ext) => ext.alias,
|
||||
(ext, i) => (this.renderMethod ? this.renderMethod(ext, i) : ext.component),
|
||||
)
|
||||
? this.single
|
||||
? this.#renderExtension(this._permitted[0], 0)
|
||||
: repeat(this._permitted, (ext) => ext.alias, this.#renderExtension)
|
||||
: html`<slot></slot>`
|
||||
: '';
|
||||
}
|
||||
|
||||
#renderExtension = (ext: UmbExtensionElementInitializer, i: number) => {
|
||||
return this.renderMethod ? this.renderMethod(ext, i) : ext.component;
|
||||
};
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: contents;
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/ext
|
||||
|
||||
@customElement('umb-test-extension-slot-manifest-element')
|
||||
class UmbTestExtensionSlotManifestElement extends HTMLElement {}
|
||||
@customElement('umb-test-extension-slot-manifest-element-2')
|
||||
class UmbTestExtensionSlotManifestElement2 extends HTMLElement {}
|
||||
|
||||
function sleep(timeMs: number) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -44,9 +46,21 @@ describe('UmbExtensionSlotElement', () => {
|
||||
expect(element).to.have.property('filter');
|
||||
});
|
||||
|
||||
it('has a props property', () => {
|
||||
expect(element).to.have.property('props');
|
||||
});
|
||||
|
||||
it('has a defaultElement property', () => {
|
||||
expect(element).to.have.property('defaultElement');
|
||||
});
|
||||
|
||||
it('has a renderMethod property', () => {
|
||||
expect(element).to.have.property('renderMethod');
|
||||
});
|
||||
|
||||
it('has a single property', () => {
|
||||
expect(element).to.have.property('single');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,6 +71,17 @@ describe('UmbExtensionSlotElement', () => {
|
||||
alias: 'unit-test-ext-slot-element-manifest',
|
||||
name: 'unit-test-extension',
|
||||
elementName: 'umb-test-extension-slot-manifest-element',
|
||||
weight: 200, // first is the heaviest and is therefor rendered first.
|
||||
meta: {
|
||||
pathname: 'test/test',
|
||||
},
|
||||
});
|
||||
umbExtensionsRegistry.register({
|
||||
type: 'dashboard',
|
||||
alias: 'unit-test-ext-slot-element-manifest-2',
|
||||
name: 'unit-test-extension-2',
|
||||
elementName: 'umb-test-extension-slot-manifest-element-2',
|
||||
weight: 100,
|
||||
meta: {
|
||||
pathname: 'test/test',
|
||||
},
|
||||
@@ -65,6 +90,7 @@ describe('UmbExtensionSlotElement', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
umbExtensionsRegistry.unregister('unit-test-ext-slot-element-manifest');
|
||||
umbExtensionsRegistry.unregister('unit-test-ext-slot-element-manifest-2');
|
||||
});
|
||||
|
||||
it('renders a manifest element', async () => {
|
||||
@@ -73,18 +99,30 @@ describe('UmbExtensionSlotElement', () => {
|
||||
await sleep(20);
|
||||
|
||||
expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(UmbTestExtensionSlotManifestElement);
|
||||
expect(element.shadowRoot!.childElementCount).to.be.equal(2);
|
||||
});
|
||||
|
||||
it('works with the filtering method', async () => {
|
||||
element = await fixture(
|
||||
html`<umb-extension-slot
|
||||
type="dashboard"
|
||||
.filter=${(x: ManifestDashboard) => x.alias === 'unit-test-ext-slot-element-manifest'}></umb-extension-slot>`,
|
||||
.filter=${(x: ManifestDashboard) =>
|
||||
x.alias === 'unit-test-ext-slot-element-manifest-2'}></umb-extension-slot>`,
|
||||
);
|
||||
|
||||
await sleep(20);
|
||||
|
||||
expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(UmbTestExtensionSlotManifestElement2);
|
||||
expect(element.shadowRoot!.childElementCount).to.be.equal(1);
|
||||
});
|
||||
|
||||
it('works with the single mode', async () => {
|
||||
element = await fixture(html`<umb-extension-slot type="dashboard" single></umb-extension-slot>`);
|
||||
|
||||
await sleep(20);
|
||||
|
||||
expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(UmbTestExtensionSlotManifestElement);
|
||||
expect(element.shadowRoot!.childElementCount).to.be.equal(1);
|
||||
});
|
||||
|
||||
it('use the render method', async () => {
|
||||
@@ -102,6 +140,7 @@ describe('UmbExtensionSlotElement', () => {
|
||||
expect(element.shadowRoot!.firstElementChild?.firstElementChild).to.be.instanceOf(
|
||||
UmbTestExtensionSlotManifestElement,
|
||||
);
|
||||
expect(element.shadowRoot!.childElementCount).to.be.equal(1);
|
||||
});
|
||||
|
||||
it('parses the props', async () => {
|
||||
@@ -117,6 +156,7 @@ describe('UmbExtensionSlotElement', () => {
|
||||
|
||||
expect((element.shadowRoot!.firstElementChild as any).testProp).to.be.equal('fooBar');
|
||||
expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(UmbTestExtensionSlotManifestElement);
|
||||
expect(element.shadowRoot!.childElementCount).to.be.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
* @augments {UmbLitElement}
|
||||
*/
|
||||
|
||||
// TODO: Refactor extension-slot and extension-with-api slot.
|
||||
// TODO: Fire change event.
|
||||
// TODO: Make property that reveals the amount of displayed/permitted extensions.
|
||||
@customElement('umb-extension-with-api-slot')
|
||||
@@ -27,6 +28,9 @@ export class UmbExtensionWithApiSlotElement extends UmbLitElement {
|
||||
@state()
|
||||
private _permitted?: Array<UmbExtensionElementAndApiInitializer>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
single?: boolean;
|
||||
|
||||
/**
|
||||
* The type or types of extensions to render.
|
||||
* @type {string | string[]}
|
||||
@@ -177,15 +181,17 @@ export class UmbExtensionWithApiSlotElement extends UmbLitElement {
|
||||
override render() {
|
||||
return this._permitted
|
||||
? this._permitted.length > 0
|
||||
? repeat(
|
||||
this._permitted,
|
||||
(ext) => ext.alias,
|
||||
(ext, i) => (this.renderMethod ? this.renderMethod(ext, i) : ext.component),
|
||||
)
|
||||
? this.single
|
||||
? this.#renderExtension(this._permitted[0], 0)
|
||||
: repeat(this._permitted, (ext) => ext.alias, this.#renderExtension)
|
||||
: html`<slot></slot>`
|
||||
: '';
|
||||
}
|
||||
|
||||
#renderExtension = (ext: UmbExtensionElementAndApiInitializer, i: number) => {
|
||||
return this.renderMethod ? this.renderMethod(ext, i) : ext.component;
|
||||
};
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: contents;
|
||||
|
||||
@@ -195,7 +195,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i
|
||||
return html`
|
||||
<uui-box class="uui-text">
|
||||
<div class="container">
|
||||
<uui-form-validation-message>
|
||||
<umb-form-validation-message>
|
||||
<uui-input
|
||||
id="name-input"
|
||||
name="name"
|
||||
@@ -208,8 +208,8 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i
|
||||
${umbFocus()}>
|
||||
<!-- TODO: validation for bad characters -->
|
||||
</uui-input>
|
||||
</uui-form-validation-message>
|
||||
<uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
<umb-form-validation-message>
|
||||
<uui-input-lock
|
||||
id="alias-input"
|
||||
name="alias"
|
||||
@@ -222,7 +222,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i
|
||||
@input=${this.#onAliasChange}
|
||||
@lock-change=${this.#onToggleAliasLock}>
|
||||
</uui-input-lock>
|
||||
</uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
<uui-textarea
|
||||
id="description-input"
|
||||
name="description"
|
||||
@@ -231,13 +231,13 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i
|
||||
placeholder=${this.localize.term('placeholders_enterDescription')}
|
||||
.value=${this._data?.description}></uui-textarea>
|
||||
</div>
|
||||
<uui-form-validation-message>
|
||||
<umb-form-validation-message>
|
||||
<umb-data-type-flow-input
|
||||
.value=${this._data?.dataType?.unique ?? ''}
|
||||
@change=${this.#onDataTypeIdChange}
|
||||
required
|
||||
${umbBindToValidation(this, '$.dataType.unique')}></umb-data-type-flow-input>
|
||||
</uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
<hr />
|
||||
<div class="container">
|
||||
<b><umb-localize key="validation_validation">Validation</umb-localize></b>
|
||||
|
||||
@@ -81,9 +81,9 @@ export class UmbPropertyLayoutElement extends UmbLitElement {
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
<div id="editorColumn">
|
||||
<uui-form-validation-message>
|
||||
<umb-form-validation-message>
|
||||
<slot name="editor"></slot>
|
||||
</uui-form-validation-message>
|
||||
</umb-form-validation-message>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export class UmbPropertyElement extends UmbLitElement {
|
||||
if ('checkValidity' in this._element) {
|
||||
const dataPath = this.dataPath;
|
||||
this.#controlValidator = new UmbFormControlValidator(this, this._element as any, dataPath);
|
||||
// We trust blindly that the dataPath is available at this stage. [NL]
|
||||
// We trust blindly that the dataPath will be present at this stage and not arrive later than this moment. [NL]
|
||||
if (dataPath) {
|
||||
this.#validationMessageBinder = new UmbBindServerValidationToFormControl(
|
||||
this,
|
||||
|
||||
@@ -16,9 +16,7 @@ class UmbSorterTestElement extends UmbLitElement {
|
||||
itemSelector: '.item',
|
||||
containerSelector: '#container',
|
||||
disabledItemSelector: '.disabled',
|
||||
onChange: ({ model }) => {
|
||||
this.model = model;
|
||||
},
|
||||
// TODO: In theory missing model change callback? [NL]
|
||||
});
|
||||
|
||||
getAllItems() {
|
||||
@@ -151,6 +149,7 @@ describe('UmbSorterController', () => {
|
||||
|
||||
describe('enable', () => {
|
||||
it('sets all allowed items to draggable', () => {
|
||||
// [NL] I have experienced an issue with this test, it may need a little delay before testing for this. As the test relies on DOM.
|
||||
const items = element.getSortableItems();
|
||||
expect(items.length).to.equal(3);
|
||||
items.forEach((item) => {
|
||||
|
||||
@@ -20,6 +20,9 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
|
||||
selection: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
_hasSelection: boolean = false;
|
||||
|
||||
@state()
|
||||
_createPath?: string;
|
||||
|
||||
@@ -34,6 +37,9 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
|
||||
constructor() {
|
||||
super();
|
||||
this.#pickerContext.selection.setSelectable(true);
|
||||
this.observe(this.#pickerContext.selection.hasSelection, (hasSelection) => {
|
||||
this._hasSelection = hasSelection;
|
||||
});
|
||||
this.#observePickerSelection();
|
||||
this.#observeSearch();
|
||||
}
|
||||
@@ -188,7 +194,8 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
|
||||
label=${this.localize.term('general_choose')}
|
||||
look="primary"
|
||||
color="positive"
|
||||
@click=${this._submitModal}></uui-button>
|
||||
@click=${this._submitModal}
|
||||
?disabled=${!this._hasSelection}></uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export class UmbSelectionManager<ValueType extends string | null = string | null
|
||||
|
||||
#selection = new UmbArrayState(<Array<ValueType>>[], (x) => x);
|
||||
public readonly selection = this.#selection.asObservable();
|
||||
public readonly hasSelection = this.#selection.asObservablePart((x) => x.length > 0);
|
||||
|
||||
#multiple = new UmbBooleanState(false);
|
||||
public readonly multiple = this.#multiple.asObservable();
|
||||
|
||||
@@ -126,4 +126,28 @@ This fact enables a property to observe if there is any Message Paths that start
|
||||
Validators represent a component of the Validation to be considered, but it does not represent other messages of its path.
|
||||
To display messages from a given data-path, a Binder is needed. We bring a few to make this happen:
|
||||
|
||||
UmbBindServerValidationToFormControl
|
||||
### UmbBindServerValidationToFormControl
|
||||
|
||||
This binder takes a Form Control Element and a data-path.
|
||||
The Data Path is a JSON Path defining where the data of this input is located in the model sent to the server.
|
||||
|
||||
```
|
||||
this.#validationMessageBinder = new UmbBindServerValidationToFormControl(
|
||||
this,
|
||||
this.querySelector('#myInput"),
|
||||
"$.values.[?(@.alias = 'my-input-alias')].value",
|
||||
);
|
||||
```
|
||||
|
||||
Once the binder is initialized you need to keep it updated with the value your form control represents. Notice we do not recommend using events from the form control to notify about the changes.
|
||||
Instead observe the value in of your data model.
|
||||
|
||||
This example is just a dummy example of how that could look:
|
||||
```
|
||||
this.observe(
|
||||
this.#value,
|
||||
(value) => {
|
||||
this.#validationMessageBinder.value = value;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { UmbValidationInvalidEvent, UmbValidationValidEvent } from '../events/index.js';
|
||||
import type { UmbFormControlMixinInterface } from '../mixins/index.js';
|
||||
import { css, customElement, html, property, repeat, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
/**
|
||||
* @description - Component for displaying one or more validation messages from UMB/UUI Form Control within the given scope.
|
||||
* Notice: Only supports components that is build on the UMB / UUI FormControlMixing.
|
||||
* @slot - for button contents
|
||||
* @slot message - for extras in the messages container
|
||||
* @see FormControlMixin
|
||||
*/
|
||||
@customElement('umb-form-validation-message')
|
||||
export class UmbFormValidationMessageElement extends UmbLitElement {
|
||||
/**
|
||||
* Set the element containing Form Controls of interest.
|
||||
* @type {string}
|
||||
* @default
|
||||
*/
|
||||
@property({ reflect: false, attribute: true })
|
||||
public get for(): HTMLElement | string | null {
|
||||
return this._for;
|
||||
}
|
||||
public set for(value: HTMLElement | string | null) {
|
||||
let element = null;
|
||||
if (typeof value === 'string') {
|
||||
const scope = this.getRootNode();
|
||||
element = (scope as DocumentFragment)?.getElementById(value);
|
||||
} else if (value instanceof HTMLElement) {
|
||||
element = value;
|
||||
}
|
||||
const newScope = element ?? this;
|
||||
const oldScope = this._for;
|
||||
|
||||
if (oldScope === newScope) {
|
||||
return;
|
||||
}
|
||||
if (oldScope !== null) {
|
||||
oldScope.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#onControlInvalid as EventListener);
|
||||
oldScope.removeEventListener(UmbValidationValidEvent.TYPE, this.#onControlValid as EventListener);
|
||||
}
|
||||
this._for = newScope;
|
||||
this._for.addEventListener(UmbValidationInvalidEvent.TYPE, this.#onControlInvalid as EventListener);
|
||||
this._for.addEventListener(UmbValidationValidEvent.TYPE, this.#onControlValid as EventListener);
|
||||
}
|
||||
private _for: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (this.for === null) {
|
||||
this.for = this;
|
||||
}
|
||||
}
|
||||
|
||||
private _messages = new Map<UmbFormControlMixinInterface<unknown>, string>();
|
||||
|
||||
#onControlInvalid = async (e: UmbValidationInvalidEvent) => {
|
||||
const ctrl = (e as any).composedPath()[0];
|
||||
if (ctrl.pristine === false) {
|
||||
// Currently we only show message from components who does have the pristine property. (we only want to show messages from fields that are NOT pristine aka. that are dirty or in a from that has been submitted)
|
||||
// Notice we use the localization controller here, this is different frm the UUI component which uses the same name.
|
||||
this._messages.set(ctrl, this.localize.string(ctrl.validationMessage));
|
||||
} else {
|
||||
this._messages.delete(ctrl);
|
||||
}
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
#onControlValid = (e: UmbValidationValidEvent) => {
|
||||
const ctrl = (e as any).composedPath()[0];
|
||||
this._messages.delete(ctrl);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<slot></slot>
|
||||
<div id="messages">
|
||||
${repeat(this._messages, (item) => html`<div>${unsafeHTML(item[1])}</div>`)}
|
||||
<slot name="message"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
#messages {
|
||||
color: var(--uui-color-danger-standalone);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-form-validation-message': UmbFormValidationMessageElement;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from './components/form-validation-message.element.js';
|
||||
export * from './const.js';
|
||||
export * from './context/index.js';
|
||||
export * from './controllers/index.js';
|
||||
export * from './directives/bind-to-validation.lit-directive.js';
|
||||
export * from './events/index.js';
|
||||
export * from './interfaces/index.js';
|
||||
export * from './mixins/index.js';
|
||||
export * from './translators/index.js';
|
||||
export * from './utils/index.js';
|
||||
export * from './directives/bind-to-validation.lit-directive.js';
|
||||
|
||||
@@ -80,6 +80,7 @@ export declare abstract class UmbFormControlMixinElement<ValueType>
|
||||
) => UmbFormControlValidatorConfig;
|
||||
removeValidator: (obj: UmbFormControlValidatorConfig) => void;
|
||||
protected addFormControlElement(element: UmbNativeFormControlElement): void;
|
||||
protected removeFormControlElement(element: UmbNativeFormControlElement): void;
|
||||
|
||||
//static formAssociated: boolean;
|
||||
protected getFormElement(): HTMLElement | undefined | null;
|
||||
@@ -96,10 +97,11 @@ export declare abstract class UmbFormControlMixinElement<ValueType>
|
||||
}
|
||||
|
||||
/**
|
||||
* @mixin
|
||||
* The mixin allows a custom element to participate in HTML forms.
|
||||
* @param {object} superClass - superclass to be extended.
|
||||
* @param defaultValue
|
||||
* @mixin
|
||||
* @param {object} defaultValue - Default value for the form control.
|
||||
* @returns {Function} - The mixin class.
|
||||
*/
|
||||
export function UmbFormControlMixin<
|
||||
ValueType = FormData | FormDataEntryValue,
|
||||
@@ -172,7 +174,7 @@ export function UmbFormControlMixin<
|
||||
* Get internal form element.
|
||||
* This has to be implemented to provide a FormControl Element of choice for the given context. The element is used as anchor for validation-messages.
|
||||
* @function getFormElement
|
||||
* @returns {HTMLElement | undefined | null}
|
||||
* @returns {HTMLElement | undefined | null} - Returns the form element or undefined if not found.
|
||||
*/
|
||||
protected getFormElement(): HTMLElement | undefined | null {
|
||||
return this.#formCtrlElements.find((el) => el.validity.valid === false);
|
||||
@@ -181,7 +183,7 @@ export function UmbFormControlMixin<
|
||||
/**
|
||||
* Focus first element that is invalid.
|
||||
* @function focusFirstInvalidElement
|
||||
* @returns {HTMLElement | undefined}
|
||||
* @returns {HTMLElement | undefined} - Returns the first invalid element or undefined if no invalid elements are found.
|
||||
*/
|
||||
focusFirstInvalidElement() {
|
||||
const firstInvalid = this.#formCtrlElements.find((el) => el.validity.valid === false);
|
||||
@@ -219,6 +221,7 @@ export function UmbFormControlMixin<
|
||||
* @param {FlagTypes} flagKey the type of validation.
|
||||
* @param {method} getMessageMethod method to retrieve relevant message. Is executed every time the validator is re-executed.
|
||||
* @param {method} checkMethod method to determine if this validator should invalidate this form control. Return true if this should prevent submission.
|
||||
* @returns {UmbFormControlValidatorConfig} - The added validator configuration.
|
||||
*/
|
||||
addValidator(
|
||||
flagKey: FlagTypes,
|
||||
@@ -249,19 +252,18 @@ export function UmbFormControlMixin<
|
||||
}
|
||||
}
|
||||
|
||||
#runValidatorsCallback = () => this._runValidators;
|
||||
|
||||
/**
|
||||
* @function addFormControlElement
|
||||
* @description Important notice if adding a native form control then ensure that its value and thereby validity is updated when value is changed from the outside.
|
||||
* @param element {UmbNativeFormControlElement} - element to validate and include as part of this form association.
|
||||
* @param {UmbNativeFormControlElement} element - element to validate and include as part of this form control association.
|
||||
* @returns {void}
|
||||
*/
|
||||
protected addFormControlElement(element: UmbNativeFormControlElement) {
|
||||
this.#formCtrlElements.push(element);
|
||||
element.addEventListener(UmbValidationInvalidEvent.TYPE, () => {
|
||||
this._runValidators();
|
||||
});
|
||||
element.addEventListener(UmbValidationValidEvent.TYPE, () => {
|
||||
this._runValidators();
|
||||
});
|
||||
element.addEventListener(UmbValidationInvalidEvent.TYPE, this.#runValidatorsCallback);
|
||||
element.addEventListener(UmbValidationValidEvent.TYPE, this.#runValidatorsCallback);
|
||||
// If we are in validationMode/'touched'/not-pristine then we need to validate this newly added control. [NL]
|
||||
if (this._pristine === false) {
|
||||
element.checkValidity();
|
||||
@@ -270,12 +272,29 @@ export function UmbFormControlMixin<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @function removeFormControlElement
|
||||
* @param {UmbNativeFormControlElement} element - element to remove as part of this form controls associated controls.
|
||||
* @returns {void}
|
||||
*/
|
||||
protected removeFormControlElement(element: UmbNativeFormControlElement) {
|
||||
const index = this.#formCtrlElements.indexOf(element);
|
||||
if (index !== -1) {
|
||||
this.#formCtrlElements.splice(index, 1);
|
||||
element.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#runValidatorsCallback);
|
||||
element.removeEventListener(UmbValidationValidEvent.TYPE, this.#runValidatorsCallback);
|
||||
if (this._pristine === false) {
|
||||
this._runValidators();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _customValidityObject?: UmbFormControlValidatorConfig;
|
||||
|
||||
/**
|
||||
* @function setCustomValidity
|
||||
* @description Set custom validity state, set to empty string to remove the custom message.
|
||||
* @param message {string} - The message to be shown
|
||||
* @param {string} message - The message to be shown
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity|HTMLObjectElement:setCustomValidity}
|
||||
*/
|
||||
protected setCustomValidity(message: string | null) {
|
||||
|
||||
@@ -105,6 +105,9 @@ export class UmbDataTypeWorkspaceViewInfoReferenceElement extends UmbLitElement
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
uui-table-cell {
|
||||
color: var(--uui-color-text-alt);
|
||||
}
|
||||
|
||||
@@ -97,16 +97,14 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement {
|
||||
|
||||
override render() {
|
||||
return html`<uui-box>
|
||||
<div id="rollback" slot="header">
|
||||
<h2><umb-localize key="general_history">History</umb-localize></h2>
|
||||
<uui-button
|
||||
label=${this.localize.term('actions_rollback')}
|
||||
look="secondary"
|
||||
slot="actions"
|
||||
@click=${this.#onRollbackModalOpen}>
|
||||
<uui-icon name="icon-undo"></uui-icon> ${this.localize.term('actions_rollback')}
|
||||
</uui-button>
|
||||
</div>
|
||||
<umb-localize slot="headline" key="general_history">History</umb-localize>
|
||||
<uui-button
|
||||
slot="header-actions"
|
||||
label=${this.localize.term('actions_rollback')}
|
||||
look="secondary"
|
||||
@click=${this.#onRollbackModalOpen}>
|
||||
<uui-icon name="icon-undo"></uui-icon> ${this.localize.term('actions_rollback')}
|
||||
</uui-button>
|
||||
${this._items ? this.#renderHistory() : html`<uui-loader-circle></uui-loader-circle> `}
|
||||
${this.#renderPagination()}
|
||||
</uui-box> `;
|
||||
@@ -167,18 +165,6 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#rollback {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#rollback h2 {
|
||||
font-size: var(--uui-type-h5-size);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
uui-tag uui-icon {
|
||||
margin-right: var(--uui-size-space-1);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,9 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
uui-table-cell:not(.link-cell) {
|
||||
color: var(--uui-color-text-alt);
|
||||
}
|
||||
|
||||
@@ -196,6 +196,10 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement {
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
uui-table-cell {
|
||||
color: var(--uui-color-text-alt);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { UmbChangeUserPasswordRepository } from '../../repository/index.js';
|
||||
import { UmbChangeUserPasswordRepository } from '@umbraco-cms/backoffice/user';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CHANGE_PASSWORD_MODAL } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserRepository } from '@umbraco-cms/backoffice/current-user';
|
||||
|
||||
export class UmbChangeUserPasswordEntityAction extends UmbEntityActionBase<never> {
|
||||
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<never>) {
|
||||
@@ -23,8 +24,16 @@ export class UmbChangeUserPasswordEntityAction extends UmbEntityActionBase<never
|
||||
|
||||
const data = await modalContext.onSubmit();
|
||||
|
||||
const repository = new UmbChangeUserPasswordRepository(this);
|
||||
await repository.changePassword(this.args.unique, data.newPassword);
|
||||
const currentUserContext = await this.getContext(UMB_CURRENT_USER_CONTEXT);
|
||||
const isCurrentUser = await currentUserContext.isUserCurrentUser(this.args.unique);
|
||||
|
||||
if (isCurrentUser) {
|
||||
const repository = new UmbCurrentUserRepository(this);
|
||||
await repository.changePassword(data.newPassword, data.oldPassword);
|
||||
} else {
|
||||
const repository = new UmbChangeUserPasswordRepository(this);
|
||||
await repository.changePassword(this.args.unique, data.newPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UMB_USER_ENTITY_TYPE } from '@umbraco-cms/backoffice/user';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
{
|
||||
type: 'entityAction',
|
||||
kind: 'default',
|
||||
alias: 'Umb.EntityAction.User.ChangePassword',
|
||||
name: 'Change User Password Entity Action',
|
||||
weight: 600,
|
||||
api: () => import('./change-user-password.action.js'),
|
||||
forEntityTypes: [UMB_USER_ENTITY_TYPE],
|
||||
meta: {
|
||||
icon: 'icon-key',
|
||||
label: '#user_changePassword',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.ChangePassword',
|
||||
name: 'Change Password Modal',
|
||||
js: () => import('./change-password-modal.element.js'),
|
||||
},
|
||||
];
|
||||
@@ -2,6 +2,8 @@ export * from './action/index.js';
|
||||
export * from './components/index.js';
|
||||
export * from './history/current-user-history.store.js';
|
||||
export * from './utils/index.js';
|
||||
export * from './repository/index.js';
|
||||
export * from './current-user.context.js';
|
||||
export * from './current-user.context.token.js';
|
||||
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { UMB_CURRENT_USER_CONTEXT } from '../current-user.context.token.js';
|
||||
import { UmbCurrentUserRepository } from '../repository/index.js';
|
||||
import { UmbActionBase } from '@umbraco-cms/backoffice/action';
|
||||
import type { UmbCurrentUserAction, UmbCurrentUserActionArgs } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UMB_CHANGE_PASSWORD_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export class UmbChangePasswordCurrentUserAction<ArgsMetaType = never>
|
||||
extends UmbActionBase<UmbCurrentUserActionArgs<ArgsMetaType>>
|
||||
implements UmbCurrentUserAction<ArgsMetaType>
|
||||
@@ -32,13 +32,17 @@ export class UmbChangePasswordCurrentUserAction<ArgsMetaType = never>
|
||||
if (!this.#unique) return;
|
||||
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
modalManager.open(this, UMB_CHANGE_PASSWORD_MODAL, {
|
||||
const modalContext = modalManager.open(this, UMB_CHANGE_PASSWORD_MODAL, {
|
||||
data: {
|
||||
user: {
|
||||
unique: this.#unique,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = await modalContext.onSubmit();
|
||||
const repository = new UmbCurrentUserRepository(this);
|
||||
await repository.changePassword(data.newPassword, data.oldPassword);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { UmbCurrentUserServerDataSource } from './current-user.server.data-sourc
|
||||
import { UMB_CURRENT_USER_STORE_CONTEXT } from './current-user.store.token.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
|
||||
import type { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
/**
|
||||
* A repository for the current user
|
||||
@@ -12,6 +14,7 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
#currentUserSource = new UmbCurrentUserServerDataSource(this._host);
|
||||
#currentUserStore?: typeof UMB_CURRENT_USER_STORE_CONTEXT.TYPE;
|
||||
#init: Promise<unknown>;
|
||||
protected notificationContext?: UmbNotificationContext;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
@@ -20,6 +23,10 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT, (instance) => {
|
||||
this.#currentUserStore = instance;
|
||||
}).asPromise(),
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
|
||||
this.notificationContext = instance;
|
||||
}).asPromise(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -108,6 +115,27 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* Change password for current user
|
||||
* @param userId
|
||||
* @param newPassword
|
||||
* @param oldPassword
|
||||
* @param isCurrentUser
|
||||
* @returns
|
||||
*/
|
||||
async changePassword(newPassword: string, oldPassword: string) {
|
||||
if (!newPassword) throw new Error('New password is missing');
|
||||
if (!oldPassword) throw new Error('Old password is missing');
|
||||
|
||||
const { data, error } = await this.#currentUserSource.changePassword(newPassword, oldPassword);
|
||||
|
||||
if (!error) {
|
||||
const notification = { data: { message: `Password changed` } };
|
||||
this.notificationContext?.peek('positive', notification);
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbCurrentUserRepository;
|
||||
|
||||
@@ -115,4 +115,24 @@ export class UmbCurrentUserServerDataSource {
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password for current user
|
||||
* @param id
|
||||
* @param newPassword
|
||||
* @param oldPassword
|
||||
* @param isCurrentUser
|
||||
* @returns
|
||||
*/
|
||||
async changePassword(newPassword: string, oldPassword: string) {
|
||||
return tryExecuteAndNotify(
|
||||
this.#host,
|
||||
UserService.postCurrentUserByIdChangePassword({
|
||||
requestBody: {
|
||||
newPassword,
|
||||
oldPassword
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { UMB_CURRENT_USER_REPOSITORY_ALIAS } from './constants.js';
|
||||
export { UMB_CURRENT_USER_STORE_CONTEXT } from './current-user.store.token.js';
|
||||
export { UmbCurrentUserRepository } from './current-user.repository.js';
|
||||
export { UmbCurrentUserStore } from './current-user.store.js';
|
||||
export { UmbCurrentUserStore } from './current-user.store.js';
|
||||
@@ -3,7 +3,7 @@ import { manifests as userManifests } from './user/manifests.js';
|
||||
import { manifests as userSectionManifests } from './user-section/manifests.js';
|
||||
import { manifests as currentUserManifests } from './current-user/manifests.js';
|
||||
import { manifests as userPermissionManifests } from './user-permission/manifests.js';
|
||||
import { manifests as modalManifests } from './modals/manifests.js';
|
||||
import { manifests as changePasswordManifests } from './change-password/manifests.js';
|
||||
|
||||
// We need to load any components that are not loaded by the user management bundle to register them in the browser.
|
||||
import './user-group/components/index.js';
|
||||
@@ -16,5 +16,5 @@ export const manifests = [
|
||||
...userSectionManifests,
|
||||
...currentUserManifests,
|
||||
...userPermissionManifests,
|
||||
...modalManifests,
|
||||
...changePasswordManifests,
|
||||
];
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
const modals: Array<ManifestModal> = [
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.ChangePassword',
|
||||
name: 'Change Password Modal',
|
||||
js: () => import('./change-password/change-password-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [...modals];
|
||||
@@ -55,19 +55,6 @@ const entityActions: Array<ManifestTypes> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
kind: 'default',
|
||||
alias: 'Umb.EntityAction.User.ChangePassword',
|
||||
name: 'Change User Password Entity Action',
|
||||
weight: 600,
|
||||
api: () => import('./change-password/change-user-password.action.js'),
|
||||
forEntityTypes: [UMB_USER_ENTITY_TYPE],
|
||||
meta: {
|
||||
icon: 'icon-key',
|
||||
label: '#user_changePassword',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
kind: 'default',
|
||||
|
||||
@@ -33,7 +33,7 @@ export class UmbChangeUserPasswordServerDataSource {
|
||||
UserService.postUserByIdChangePassword({
|
||||
id,
|
||||
requestBody: {
|
||||
newPassword,
|
||||
newPassword
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user