reuse generic picker logic for input-section

This commit is contained in:
Mads Rasmussen
2024-03-07 11:34:34 +01:00
parent 702ffd1b4a
commit 4e03763e3a
7 changed files with 142 additions and 70 deletions

View File

@@ -6,4 +6,4 @@ export * from './section-sidebar-menu-with-entity-actions/index.js';
export * from './section-default.element.js';
export * from './section.context.js';
export * from './input-section/index.js';
export * from './section-picker/section-picker-modal.token.js';
export * from './section-picker-modal/section-picker-modal.token.js';

View File

@@ -0,0 +1,11 @@
import type { UmbSectionItemModel } from '../repository/index.js';
import { UMB_SECTION_ITEM_REPOSITORY_ALIAS } from '../repository/index.js';
import { UMB_SECTION_PICKER_MODAL } from '../section-picker-modal/section-picker-modal.token.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbSectionPickerContext extends UmbPickerInputContext<UmbSectionItemModel> {
constructor(host: UmbControllerHost) {
super(host, UMB_SECTION_ITEM_REPOSITORY_ALIAS, UMB_SECTION_PICKER_MODAL);
}
}

View File

@@ -1,89 +1,137 @@
import { UmbInputListBaseElement } from '../../components/input-list-base/input-list-base.js';
import { UMB_SECTION_PICKER_MODAL } from '../section-picker/section-picker-modal.token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbSectionItemModel } from '../repository/index.js';
import { UmbSectionPickerContext } from './input-section.context.js';
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
@customElement('umb-input-section')
export class UmbInputSectionElement extends UmbInputListBaseElement {
export class UmbInputSectionElement extends FormControlMixin(UmbLitElement) {
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default 0
*/
@property({ type: Number })
public get min(): number {
return this.#pickerContext.min;
}
public set min(value: number) {
this.#pickerContext.min = value;
}
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default Infinity
*/
@property({ type: Number })
public get max(): number {
return this.#pickerContext.max;
}
public set max(value: number) {
this.#pickerContext.max = value;
}
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
public get selection(): Array<string> {
return this.#pickerContext.getSelection();
}
public set selection(uniques: Array<string>) {
this.#pickerContext.setSelection(uniques);
}
@property()
public set value(selectionString: string) {
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
if (typeof selectionString !== 'string') return;
if (selectionString === this.value) return;
this.selection = splitStringToArray(selectionString);
}
@state()
private _sections: Array<ManifestSection> = [];
private _items?: Array<UmbSectionItemModel>;
connectedCallback(): void {
super.connectedCallback();
this.pickerToken = UMB_SECTION_PICKER_MODAL;
this._observeSections();
#pickerContext = new UmbSectionPickerContext(this);
constructor() {
super();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this.#pickerContext.getSelection().length < this.min,
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
);
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
private _observeSections() {
if (this.value.length > 0) {
this.observe(umbExtensionsRegistry.byType('section'), (sections: Array<ManifestSection>) => {
this._sections = sections.filter((section) => this.value.includes(section.alias));
});
} else {
this._sections = [];
}
protected getFormElement() {
return undefined;
}
selectionUpdated() {
this._observeSections();
// TODO: Use proper event class:
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
renderContent() {
if (this._sections.length === 0) return html`${nothing}`;
render() {
return html`
<div id="user-list">
${this._sections.map(
(section) => html`
<div class="user-group">
<div>
<span>${section.meta.label}</span>
</div>
<uui-button
@click=${() => this.removeFromSelection(section.alias)}
label="remove"
color="danger"></uui-button>
</div>
`,
)}
</div>
<uui-ref-list>${this._items?.map((item) => this._renderItem(item))}</uui-ref-list>
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open"
>Add</uui-button
>
`;
}
private _renderItem(item: UmbSectionItemModel) {
if (!item.unique) return;
return html`${item.unique}
<!--
<uui-ref-node-data-type name=${item.name}>
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.unique)}
label="Remove Data Type ${item.name}"
>Remove</uui-button
>
</uui-action-bar>
</uui-ref-node-data-type>
-->`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-group-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user-group {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user-group div {
display: flex;
align-items: center;
gap: var(--uui-size-4);
}
.user-group uui-button {
margin-left: auto;
#add-button {
width: 100%;
}
`,
];
}
export default UmbInputSectionElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-section': UmbInputSectionElement;

View File

@@ -1,8 +1,11 @@
import { manifests as repositoryManifests } from './repository/manifests.js';
export const manifests = [
{
type: 'modal',
alias: 'Umb.Modal.SectionPicker',
name: 'Section Picker Modal',
js: () => import('./section-picker/section-picker-modal.element.js'),
js: () => import('./section-picker-modal/section-picker-modal.element.js'),
},
...repositoryManifests,
];

View File

@@ -13,14 +13,24 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
@state()
private _sections: Array<ManifestSection> = [];
@state()
private _selectable = false;
#selectionManager = new UmbSelectionManager(this);
constructor() {
super();
this.observe(this.#selectionManager.selectable, (selectable) => (this._selectable = selectable));
}
connectedCallback(): void {
super.connectedCallback();
// TODO: in theory this config could change during the lifetime of the modal, so we could observe it
this.#selectionManager.setMultiple(this.data?.multiple ?? false);
this.#selectionManager.setSelection(this.data?.selection ?? []);
this.#selectionManager.setSelectable(true);
this.observe(
umbExtensionsRegistry.byType('section'),
@@ -42,7 +52,7 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
(item) => html`
<uui-menu-item
label=${item.meta.label}
selectable
?selectable=${this._selectable}
?selected=${this.#selectionManager.isSelected(item.alias)}
@selected=${() => this.#selectionManager.select(item.alias)}
@deselected=${() => this.#selectionManager.deselect(item.alias)}></uui-menu-item>