Merge branch 'main' into bugfix/relation-types
This commit is contained in:
@@ -135,6 +135,7 @@ Ensure all property editors are properly localized.
|
||||
- [ ] Toggle
|
||||
- [ ] Tree Picker
|
||||
- [ ] StartNode
|
||||
- [x] DynamicRoot
|
||||
- [ ] Upload Field
|
||||
- [ ] User Picker
|
||||
- [ ] Value Type
|
||||
@@ -235,4 +236,4 @@ Then we need your help! With Bellissima we added new localization keys, and we s
|
||||
- [ ] `tr-TR` - Turkish (Turkey)
|
||||
- [ ] `ua-UA` - Ukrainian (Ukraine)
|
||||
- [ ] `zh-CN` - Chinese (China)
|
||||
- [ ] `zh-TW` - Chinese (Taiwan)
|
||||
- [ ] `zh-TW` - Chinese (Taiwan)
|
||||
|
||||
900
src/Umbraco.Web.UI.Client/package-lock.json
generated
900
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,7 @@
|
||||
"element-internals-polyfill": "^1.3.9",
|
||||
"lit": "^2.8.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"marked": "^11.1.0",
|
||||
"marked": "^11.1.1",
|
||||
"monaco-editor": "^0.45.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tinymce-i18n": "^23.12.4",
|
||||
@@ -159,13 +159,13 @@
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@storybook/addon-a11y": "7.6.5",
|
||||
"@storybook/addon-actions": "7.6.5",
|
||||
"@storybook/addon-essentials": "7.6.5",
|
||||
"@storybook/addon-links": "7.6.5",
|
||||
"@storybook/addon-a11y": "7.6.7",
|
||||
"@storybook/addon-actions": "7.6.7",
|
||||
"@storybook/addon-essentials": "7.6.7",
|
||||
"@storybook/addon-links": "7.6.7",
|
||||
"@storybook/mdx2-csf": "^1.1.0",
|
||||
"@storybook/web-components-vite": "7.6.5",
|
||||
"@storybook/web-components": "7.6.5",
|
||||
"@storybook/web-components-vite": "7.6.7",
|
||||
"@storybook/web-components": "7.6.7",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/mocha": "^10.0.1",
|
||||
@@ -199,11 +199,11 @@
|
||||
"rollup-plugin-import-css": "^3.3.5",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"rollup": "^4.9.0",
|
||||
"storybook": "7.6.5",
|
||||
"storybook": "7.6.7",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript-json-schema": "^0.62.0",
|
||||
"typescript": "^5.3.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vite": "^4.4.12",
|
||||
|
||||
@@ -1151,6 +1151,8 @@ export default {
|
||||
},
|
||||
contentPicker: {
|
||||
allowedItemTypes: 'Du kan kun vælge følgende type(r) dokumenter: %0%',
|
||||
defineDynamicRoot: 'Definer Dynamisk Udgangspunkt',
|
||||
defineRootNode: 'Vælg udgangspunkt',
|
||||
pickedTrashedItem: 'Du har valgt et dokument som er slettet eller lagt i papirkurven',
|
||||
pickedTrashedItems: 'Du har valgt dokumenter som er slettede eller lagt i papirkurven',
|
||||
},
|
||||
|
||||
@@ -1148,6 +1148,8 @@ export default {
|
||||
},
|
||||
contentPicker: {
|
||||
allowedItemTypes: 'You can only select items of type(s): %0%',
|
||||
defineDynamicRoot: 'Specify a Dynamic Root',
|
||||
defineRootNode: 'Pick root node',
|
||||
pickedTrashedItem: 'You have picked a content item currently deleted or in the recycle bin',
|
||||
pickedTrashedItems: 'You have picked content items currently deleted or in the recycle bin',
|
||||
},
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
|
||||
import { UmbInputDocumentPickerRootElement } from '@umbraco-cms/backoffice/document';
|
||||
import { html, customElement, property, css, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { FormControlMixin, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
//import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
|
||||
export type ContentType = 'content' | 'member' | 'media';
|
||||
|
||||
export type DynamicRootQueryStepType = {
|
||||
alias: string;
|
||||
anyOfDocTypeKeys: Array<string>;
|
||||
};
|
||||
|
||||
export type DynamicRootType = {
|
||||
originAlias: string;
|
||||
querySteps?: Array<DynamicRootQueryStepType> | null;
|
||||
};
|
||||
|
||||
export type StartNode = {
|
||||
type?: ContentType;
|
||||
id?: string | null;
|
||||
query?: string | null;
|
||||
dynamicRoot?: DynamicRootType | null;
|
||||
};
|
||||
|
||||
@customElement('umb-input-start-node')
|
||||
@@ -20,14 +30,21 @@ export class UmbInputStartNodeElement extends FormControlMixin(UmbLitElement) {
|
||||
}
|
||||
|
||||
private _type: StartNode['type'] = 'content';
|
||||
|
||||
@property()
|
||||
public set type(value: StartNode['type']) {
|
||||
if (value === undefined) {
|
||||
value = this._type;
|
||||
}
|
||||
|
||||
const oldValue = this._type;
|
||||
|
||||
this._options = this._options.map((option) =>
|
||||
option.value === value ? { ...option, selected: true } : { ...option, selected: false },
|
||||
);
|
||||
|
||||
this._type = value;
|
||||
|
||||
this.requestUpdate('type', oldValue);
|
||||
}
|
||||
public get type(): StartNode['type'] {
|
||||
@@ -35,30 +52,43 @@ export class UmbInputStartNodeElement extends FormControlMixin(UmbLitElement) {
|
||||
}
|
||||
|
||||
@property({ attribute: 'node-id' })
|
||||
nodeId = '';
|
||||
nodeId?: string | null;
|
||||
|
||||
@property({ attribute: 'dynamic-path' })
|
||||
dynamicPath = '';
|
||||
@property({ attribute: false })
|
||||
dynamicRoot?: DynamicRootType | null;
|
||||
|
||||
@state()
|
||||
_options: Array<Option> = [
|
||||
{ value: 'content', name: 'Content' },
|
||||
{ value: 'member', name: 'Members' },
|
||||
{ value: 'media', name: 'Media' },
|
||||
{ value: 'member', name: 'Members' },
|
||||
];
|
||||
|
||||
#onTypeChange(event: UUISelectEvent) {
|
||||
//console.log('onTypeChange');
|
||||
|
||||
this.type = event.target.value as StartNode['type'];
|
||||
|
||||
// Clear others
|
||||
this.nodeId = '';
|
||||
this.dynamicPath = '';
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
|
||||
// TODO: Appears that the event gets bubbled up. Will need to review. [LK]
|
||||
//this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
#onIdChange(event: CustomEvent) {
|
||||
this.nodeId = (event.target as UmbInputDocumentElement | UmbInputMediaElement).selectedIds.join('');
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
//console.log('onIdChange', event.target);
|
||||
switch (this.type) {
|
||||
case 'content':
|
||||
this.nodeId = (<UmbInputDocumentPickerRootElement>event.target).nodeId;
|
||||
break;
|
||||
case 'media':
|
||||
this.nodeId = (<UmbInputMediaElement>event.target).selectedIds.join('');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent(event.type));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -82,21 +112,21 @@ export class UmbInputStartNodeElement extends FormControlMixin(UmbLitElement) {
|
||||
}
|
||||
|
||||
#renderTypeContent() {
|
||||
const nodeId = this.nodeId ? [this.nodeId] : [];
|
||||
//TODO: Dynamic paths
|
||||
return html` <umb-input-document @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-document> `;
|
||||
return html`<umb-input-document-picker-root
|
||||
@change=${this.#onIdChange}
|
||||
.nodeId=${this.nodeId}></umb-input-document-picker-root>`;
|
||||
}
|
||||
|
||||
#renderTypeMedia() {
|
||||
const nodeId = this.nodeId ? [this.nodeId] : [];
|
||||
//TODO => MediaTypes
|
||||
return html` <umb-input-media @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-media> `;
|
||||
return html`<umb-input-media @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-media>`;
|
||||
}
|
||||
|
||||
#renderTypeMember() {
|
||||
const nodeId = this.nodeId ? [this.nodeId] : [];
|
||||
//TODO => Members
|
||||
return html` <umb-input-member @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-member> `;
|
||||
return html`<umb-input-member @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-member>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
@@ -155,7 +155,11 @@ export class UmbTableElement extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`<uui-table class="uui-text">
|
||||
<uui-table-column style="width: 60px;"></uui-table-column>
|
||||
<uui-table-column
|
||||
.style=${when(
|
||||
!(this.config.allowSelection === false && this.config.hideIcon === true),
|
||||
() => 'width: 60px',
|
||||
)}></uui-table-column>
|
||||
<uui-table-head>
|
||||
${this._renderHeaderCheckboxCell()} ${this.columns.map((column) => this._renderHeaderCell(column))}
|
||||
</uui-table-head>
|
||||
|
||||
@@ -101,3 +101,14 @@ export const WithHiddenIcons: Story = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHiddenIconsAndDisallowedSelections: Story = {
|
||||
args: {
|
||||
items: items,
|
||||
columns: columns,
|
||||
config: {
|
||||
allowSelection: false,
|
||||
hideIcon: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,8 +51,8 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
|
||||
?multiple=${this._multiple}></umb-tree>
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label="Close" @click=${this._rejectModal}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this._submitModal}></uui-button>
|
||||
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
|
||||
<uui-button label=${this.localize.term('general_choose')} look="primary" color="positive" @click=${this._submitModal}></uui-button>
|
||||
</div>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { UmbModalToken, UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbEntityTreeItemModel } from '@umbraco-cms/backoffice/tree';
|
||||
|
||||
export type UmbMemberTypePickerModalData = UmbTreePickerModalData<UmbEntityTreeItemModel>;
|
||||
export type UmbMemberTypePickerModalValue = UmbPickerModalValue;
|
||||
|
||||
export const UMB_MEMBER_TYPE_PICKER_MODAL = new UmbModalToken<UmbMemberTypePickerModalData, UmbMemberTypePickerModalValue>(
|
||||
'Umb.Modal.TreePicker',
|
||||
{
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
},
|
||||
data: {
|
||||
treeAlias: 'Umb.Tree.MemberType',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/exten
|
||||
|
||||
export const manifest: ManifestPropertyEditorSchema = {
|
||||
type: 'propertyEditorSchema',
|
||||
name: 'Decimal',
|
||||
name: 'Integer',
|
||||
alias: 'Umbraco.Integer',
|
||||
meta: {
|
||||
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer',
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/
|
||||
|
||||
@customElement('umb-property-editor-ui-number')
|
||||
export class UmbPropertyEditorUINumberElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
@property()
|
||||
value = '';
|
||||
@property({ type: Number })
|
||||
value: undefined | number = undefined;
|
||||
|
||||
@state()
|
||||
private _max?: number;
|
||||
@@ -26,7 +26,7 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U
|
||||
}
|
||||
|
||||
private onInput(e: InputEvent) {
|
||||
this.value = (e.target as HTMLInputElement).value;
|
||||
this.value = Number((e.target as HTMLInputElement).value);
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,17 @@ export class UmbPropertyEditorUITreePickerStartNodeElement extends UmbLitElement
|
||||
this.value = {
|
||||
type: target.type,
|
||||
id: target.nodeId,
|
||||
// TODO: Please check this makes sense, Check if we want to support XPath in this version, if not then make sure we handle DynamicRoot correct.
|
||||
query: target.dynamicPath,
|
||||
dynamicRoot: target.dynamicRoot,
|
||||
};
|
||||
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<umb-input-start-node @change="${this.#onChange}" .type=${this.value?.type}></umb-input-start-node>`;
|
||||
return html`<umb-input-start-node
|
||||
@change=${this.#onChange}
|
||||
.type=${this.value?.type}
|
||||
.nodeId=${this.value?.id}></umb-input-start-node>`;
|
||||
}
|
||||
|
||||
static styles = [UmbTextStyles];
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './input-document/input-document.element.js';
|
||||
export * from './input-document-granular-permission/input-document-granular-permission.element.js';
|
||||
export * from './input-document-picker-root/input-document-picker-root.element.js';
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { UmbDocumentPickerContext } from '../input-document/input-document.context.js';
|
||||
import { html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
@customElement('umb-input-document-picker-root')
|
||||
export class UmbInputDocumentPickerRootElement extends FormControlMixin(UmbLitElement) {
|
||||
public get nodeId(): string | null | undefined {
|
||||
return this.#documentPickerContext.getSelection()[0];
|
||||
}
|
||||
public set nodeId(id: string | null | undefined) {
|
||||
const selection = id ? [id] : [];
|
||||
this.#documentPickerContext.setSelection(selection);
|
||||
}
|
||||
|
||||
@property()
|
||||
public set value(id: string) {
|
||||
this.nodeId = id;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _items?: Array<DocumentItemResponseModel>;
|
||||
|
||||
#documentPickerContext = new UmbDocumentPickerContext(this);
|
||||
|
||||
// TODO: DynamicRoot - once feature implemented, wire up context and picker UI. [LK]
|
||||
#dynamicRootPickerContext = {
|
||||
openPicker: () => {
|
||||
throw new Error('DynamicRoot picker has not been implemented yet.');
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#documentPickerContext.max = 1;
|
||||
|
||||
this.observe(this.#documentPickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#documentPickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this._items
|
||||
? html` <uui-ref-list
|
||||
>${repeat(
|
||||
this._items,
|
||||
(item) => item.id,
|
||||
(item) => this._renderItem(item),
|
||||
)}
|
||||
</uui-ref-list>`
|
||||
: ''}
|
||||
${this.#renderButtons()}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderButtons() {
|
||||
if (this.nodeId) return;
|
||||
|
||||
//TODO: Dynamic paths
|
||||
return html` <uui-button-group>
|
||||
<uui-button
|
||||
look="placeholder"
|
||||
@click=${() => this.#documentPickerContext.openPicker()}
|
||||
label=${this.localize.term('contentPicker_defineRootNode')}></uui-button>
|
||||
<uui-button
|
||||
look="placeholder"
|
||||
@click=${() => this.#dynamicRootPickerContext.openPicker()}
|
||||
label=${this.localize.term('contentPicker_defineDynamicRoot')}></uui-button>
|
||||
</uui-button-group>`;
|
||||
}
|
||||
|
||||
private _renderItem(item: DocumentItemResponseModel) {
|
||||
if (!item.id) return;
|
||||
return html`
|
||||
<uui-ref-node name=${ifDefined(item.name)} detail=${ifDefined(item.id)}>
|
||||
<!-- TODO: implement is trashed <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> -->
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button @click=${() => this.#documentPickerContext.openPicker()} label="Edit document ${item.name}"
|
||||
>Edit</uui-button
|
||||
>
|
||||
<uui-button
|
||||
@click=${() => this.#documentPickerContext.requestRemoveItem(item.id!)}
|
||||
label="Remove document ${item.name}"
|
||||
>Remove</uui-button
|
||||
>
|
||||
</uui-action-bar>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbInputDocumentPickerRootElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-document-picker-root': UmbInputDocumentPickerRootElement;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
|
||||
id="add-button"
|
||||
look="placeholder"
|
||||
@click=${() => this.#pickerContext.openPicker()}
|
||||
label=${this.localize.term('general_add')}></uui-button>`;
|
||||
label=${this.localize.term('general_choose')}></uui-button>`;
|
||||
}
|
||||
|
||||
private _renderItem(item: DocumentItemResponseModel) {
|
||||
@@ -127,7 +127,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
|
||||
<uui-button
|
||||
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
|
||||
label="Remove document ${item.name}"
|
||||
>Remove</uui-button
|
||||
>${this.localize.term('general_remove')}</uui-button
|
||||
>
|
||||
</uui-action-bar>
|
||||
</uui-ref-node>
|
||||
|
||||
@@ -101,9 +101,9 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
|
||||
#renderButton() {
|
||||
if (this._items && this.max && this._items.length >= this.max) return;
|
||||
return html`
|
||||
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open">
|
||||
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label=${this.localize.term('general_choose')}>
|
||||
<uui-icon name="icon-add"></uui-icon>
|
||||
Add
|
||||
${this.localize.term('general_choose')}
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { manifests as memberGroupManifests } from './member-groups/manifests.js'
|
||||
import { manifests as memberTypeManifests } from './member-types/manifests.js';
|
||||
import { manifests as memberManifests } from './members/manifests.js';
|
||||
|
||||
import './members/components/index.js';
|
||||
|
||||
export const manifests = [
|
||||
...memberSectionManifests,
|
||||
...menuSectionManifests,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import './input-member-type/input-member-type.element.js';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { UMB_MEMBER_TYPE_PICKER_MODAL } from '../../../../core/modal/token/member-type-picker-modal.token.js';
|
||||
import { UMB_MEMBER_TYPE_REPOSITORY_ALIAS } from '../../repository/index.js';
|
||||
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
export class UmbMemberTypePickerContext extends UmbPickerInputContext<MemberTypeItemResponseModel> {
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
super(host, UMB_MEMBER_TYPE_REPOSITORY_ALIAS, UMB_MEMBER_TYPE_PICKER_MODAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { UmbMemberTypePickerContext } from './input-member-type.context.js';
|
||||
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import type { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
@customElement('umb-input-member-type')
|
||||
export class UmbMemberTypeInputElement 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 selectedIds(): Array<string> {
|
||||
return this.#pickerContext.getSelection();
|
||||
}
|
||||
public set selectedIds(ids: Array<string>) {
|
||||
this.#pickerContext.setSelection(ids);
|
||||
}
|
||||
|
||||
@property()
|
||||
public set value(idsString: string) {
|
||||
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
|
||||
this.selectedIds = splitStringToArray(idsString);
|
||||
}
|
||||
|
||||
@property()
|
||||
get pickableFilter() {
|
||||
return this.#pickerContext.pickableFilter;
|
||||
}
|
||||
set pickableFilter(newVal) {
|
||||
this.#pickerContext.pickableFilter = newVal;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _items?: Array<MemberTypeItemResponseModel>;
|
||||
|
||||
#pickerContext = new UmbMemberTypePickerContext(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
protected _openPicker() {
|
||||
this.#pickerContext.openPicker({
|
||||
hideTreeRoot: true,
|
||||
});
|
||||
}
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.#renderItems()}
|
||||
${this.#renderAddButton()}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
if (!this._items) return;
|
||||
// TODO: Add sorting. [LK]
|
||||
return html`
|
||||
<uui-ref-list
|
||||
>${repeat(
|
||||
this._items,
|
||||
(item) => item.id,
|
||||
(item) => this._renderItem(item),
|
||||
)}</uui-ref-list
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderAddButton() {
|
||||
if (this.max > 0 && this.selectedIds.length >= this.max) return;
|
||||
return html`
|
||||
<uui-button
|
||||
id="add-button"
|
||||
look="placeholder"
|
||||
@click=${this._openPicker}
|
||||
label="${this.localize.term('general_choose')}"
|
||||
>${this.localize.term('general_choose')}</uui-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItem(item: MemberTypeItemResponseModel) {
|
||||
if (!item.id) return;
|
||||
return html`
|
||||
<uui-ref-node-document-type name=${ifDefined(item.name)}>
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button
|
||||
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
|
||||
label="Remove Member Type ${item.name}"
|
||||
>${this.localize.term('general_remove')}</uui-button
|
||||
>
|
||||
</uui-action-bar>
|
||||
</uui-ref-node-document-type>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
#add-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbMemberTypeInputElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-member-type': UmbMemberTypeInputElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import './components/index.js';
|
||||
|
||||
export * from './components/index.js';
|
||||
export * from './repository/index.js';
|
||||
export * from './entity.js';
|
||||
@@ -1,13 +1,15 @@
|
||||
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
|
||||
import { manifests as menuItemManifests } from './menu-item/manifests.js';
|
||||
import { manifests as treeManifests } from './tree/manifests.js';
|
||||
import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import { manifests as treeManifests } from './tree/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
|
||||
|
||||
import './components/index.js';
|
||||
|
||||
export const manifests = [
|
||||
...entityActionsManifests,
|
||||
...menuItemManifests,
|
||||
...treeManifests,
|
||||
...repositoryManifests,
|
||||
...treeManifests,
|
||||
...workspaceManifests,
|
||||
...entityActionManifests,
|
||||
];
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { UmbMemberTypeRepository } from './member-type.repository.js';
|
||||
export * from './member-type.repository.js';
|
||||
export * from './manifests.js';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import './input-member/input-member.element.js';
|
||||
|
||||
export * from './input-member/input-member.element.js';
|
||||
@@ -0,0 +1,171 @@
|
||||
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
@customElement('umb-input-member')
|
||||
export class UmbInputMemberElement 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;
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
return Infinity;
|
||||
}
|
||||
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 selectedIds(): Array<string> {
|
||||
//return this.#pickerContext.getSelection();
|
||||
return [];
|
||||
}
|
||||
public set selectedIds(ids: Array<string>) {
|
||||
//this.#pickerContext.setSelection(ids);
|
||||
}
|
||||
|
||||
@property()
|
||||
public set value(idsString: string) {
|
||||
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
|
||||
this.selectedIds = splitStringToArray(idsString);
|
||||
}
|
||||
|
||||
@state()
|
||||
private _items?: Array<MemberItemResponseModel>;
|
||||
|
||||
// TODO: Create the `UmbMemberPickerContext` [LK]
|
||||
//#pickerContext = new UmbMemberPickerContext(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));
|
||||
}
|
||||
|
||||
protected _openPicker() {
|
||||
console.log("member.openPicker");
|
||||
// this.#pickerContext.openPicker({
|
||||
// hideTreeRoot: true,
|
||||
// });
|
||||
}
|
||||
|
||||
protected _requestRemoveItem(item: MemberItemResponseModel){
|
||||
console.log("member.requestRemoveItem", item);
|
||||
//this.#pickerContext.requestRemoveItem(item.id!);
|
||||
}
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.#renderItems()}
|
||||
${this.#renderAddButton()}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
if (!this._items) return;
|
||||
// TODO: Add sorting. [LK]
|
||||
return html`<uui-ref-list
|
||||
>${repeat(
|
||||
this._items,
|
||||
(item) => item.id,
|
||||
(item) => this._renderItem(item),
|
||||
)}
|
||||
</uui-ref-list>`;
|
||||
}
|
||||
|
||||
#renderAddButton() {
|
||||
if (this.max > 0 && this.selectedIds.length >= this.max) return;
|
||||
return html`<uui-button
|
||||
id="add-button"
|
||||
look="placeholder"
|
||||
@click=${this._openPicker}
|
||||
label=${this.localize.term('general_add')}></uui-button>`;
|
||||
}
|
||||
|
||||
private _renderItem(item: MemberItemResponseModel) {
|
||||
if (!item.id) return;
|
||||
return html`
|
||||
<uui-ref-node name=${ifDefined(item.name)} detail=${ifDefined(item.id)}>
|
||||
<!-- TODO: implement is deleted <uui-tag size="s" slot="tag" color="danger">Deleted</uui-tag> -->
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button
|
||||
@click=${() => this._requestRemoveItem(item)}
|
||||
label="Remove member ${item.name}"
|
||||
>Remove</uui-button
|
||||
>
|
||||
</uui-action-bar>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
#add-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbInputMemberElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-member': UmbInputMemberElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Meta, StoryObj } from '@storybook/web-components';
|
||||
import './input-member.element.js';
|
||||
import type { UmbInputMemberElement } from './input-member.element.js';
|
||||
|
||||
const meta: Meta<UmbInputMemberElement> = {
|
||||
title: 'Components/Inputs/Member',
|
||||
component: 'umb-input-member',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<UmbInputMemberElement>;
|
||||
export const Overview: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbInputMemberElement } from './input-member.element.js';
|
||||
import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
|
||||
describe('UmbInputMemberElement', () => {
|
||||
let element: UmbInputMemberElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html` <umb-input-member></umb-input-member> `);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbInputMemberElement);
|
||||
});
|
||||
|
||||
if ((window as any).__UMBRACO_TEST_RUN_A11Y_TEST) {
|
||||
it('passes the a11y audit', async () => {
|
||||
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1 +1,4 @@
|
||||
import './components/index.js';
|
||||
|
||||
export * from './components/index.js';
|
||||
export * from './repository/index.js';
|
||||
|
||||
@@ -8,8 +8,8 @@ export class UmbLanguageCreateEntityAction extends UmbEntityActionBase<UmbLangua
|
||||
super(host, repositoryAlias, unique);
|
||||
}
|
||||
|
||||
// TODO: Generate the href or retrieve it from something?
|
||||
async getHref() {
|
||||
return 'section/settings/workspace/language/create';
|
||||
async execute() {
|
||||
// TODO: Generate the href or retrieve it from something?
|
||||
history.pushState(null, '', `section/settings/workspace/language/create`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class UmbSelectionManager extends UmbBaseController {
|
||||
public setSelection(value: Array<string | null>) {
|
||||
if (this.getSelectable() === false) return;
|
||||
if (value === undefined) throw new Error('Value cannot be undefined');
|
||||
const newSelection = this.getMultiple() ? value : [value[0]];
|
||||
const newSelection = this.getMultiple() ? value : value.slice(0, 1);
|
||||
this.#selection.next(newSelection);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class UmbSelectionManager extends UmbBaseController {
|
||||
public setMultiple(value: boolean) {
|
||||
this.#multiple.next(value);
|
||||
|
||||
/* If multiple is set to false, and the current selection is more than one,
|
||||
/* If multiple is set to false, and the current selection is more than one,
|
||||
then we need to set the selection to the first item. */
|
||||
if (value === false && this.getSelection().length > 1) {
|
||||
this.setSelection([this.getSelection()[0]]);
|
||||
|
||||
Reference in New Issue
Block a user