Merge remote-tracking branch 'origin/main' into feature/workspace-context-as-extension

# Conflicts:
#	src/packages/core/property-editor/uis/tree-picker/config/start-node/property-editor-ui-tree-picker-start-node.element.ts
This commit is contained in:
Niels Lyngsø
2023-11-14 15:28:43 +01:00
19 changed files with 493 additions and 26 deletions

View File

@@ -200,7 +200,35 @@ export const data: Array<DataTypeResponseModel | FolderTreeItemResponseModel> =
parentId: null,
propertyEditorAlias: 'Umbraco.MultiNodeTreePicker',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker',
values: [],
values: [
{
alias: 'startNode',
value: {
type: 'content',
id: null,
},
},
{
alias: 'minNumber',
value: 0,
},
{
alias: 'maxNumber',
value: 3,
},
{
alias: 'ignoreUserStartNodes',
value: false,
},
{
alias: 'showOpenButton',
value: true,
},
{
alias: 'filter',
value: '',
},
],
},
{
type: 'data-type',

View File

@@ -608,6 +608,56 @@ export const data: Array<DocumentTypeResponseModel> = [
keepLatestVersionPerDayForDays: null,
},
},
{
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'simple-document-type-id',
alias: 'blogPost',
name: 'All property editors document type',
description: null,
icon: 'umb:item-arrangement',
allowedAsRoot: true,
variesByCulture: true,
variesBySegment: false,
isElement: false,
properties: [
{
id: '6',
containerId: 'all-properties-group-key',
alias: 'multiNodeTreePicker',
name: 'Multi Node Tree Picker',
description: '',
dataTypeId: 'dt-multiNodeTreePicker',
variesByCulture: false,
variesBySegment: false,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
],
containers: [
{
id: 'all-properties-group-key',
parentId: null,
name: 'Content',
type: 'Group',
sortOrder: 0,
},
],
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
},
{
allowedTemplateIds: [],
@@ -1008,6 +1058,15 @@ export const treeData: Array<DocumentTypeTreeItemResponseModel> = [
parentId: null,
icon: '',
},
{
name: 'Simple document type',
type: 'document-type',
hasChildren: false,
id: 'simple-document-type-id',
isContainer: false,
parentId: null,
icon: '',
},
{
name: 'Page Document Type',
type: 'document-type',

View File

@@ -536,7 +536,12 @@ export const data: Array<DocumentResponseModel> = [
],
},
{
urls: [],
urls: [
{
culture: 'en-US',
url: '/',
},
],
templateId: null,
id: 'simple-document-id',
contentTypeId: 'simple-document-type-id',
@@ -551,6 +556,14 @@ export const data: Array<DocumentResponseModel> = [
updateDate: '2023-02-06T15:32:24.957009',
},
],
values: [
{
alias: 'multiNodeTreePicker',
culture: null,
segment: null,
value: null,
},
],
},
];

View File

@@ -78,6 +78,13 @@ export const data: Array<UserGroupResponseModel> = [
documentStartNodeId: 'all-property-editors-document-id',
permissions: [UMB_USER_PERMISSION_DOCUMENT_CREATE, UMB_USER_PERMISSION_DOCUMENT_DELETE],
},
{
id: 'c630d49e-4e7b-42ea-b2bc-edc0edacb6b2',
name: 'Something',
icon: 'umb:medal',
documentStartNodeId: 'simple-document-id',
permissions: [UMB_USER_PERMISSION_DOCUMENT_CREATE, UMB_USER_PERMISSION_DOCUMENT_DELETE],
},
{
id: '9d24dc47-a4bf-427f-8a4a-b900f03b8a12',
name: 'User Group 1',

View File

@@ -0,0 +1 @@
export * from './input-content-type/index.js';

View File

@@ -0,0 +1 @@
export * from './input-content-type.element.js';

View File

@@ -0,0 +1,119 @@
import { UmbInputDocumentElement } 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';
export type ContentType = 'content' | 'member' | 'media';
export type StartNode = {
type?: ContentType;
id?: string | null;
query?: string | null;
};
@customElement('umb-input-content-type')
export class UmbInputContentTypeElement extends FormControlMixin(UmbLitElement) {
protected getFormElement() {
return undefined;
}
private _type: StartNode['type'] = 'content';
@property()
public set type(value: StartNode['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'] {
return this._type;
}
@property({ attribute: 'node-id' })
nodeId = '';
@property({ attribute: 'dynamic-path' })
dynamicPath = '';
@state()
_options: Array<Option> = [
{ value: 'content', name: 'Content' },
{ value: 'member', name: 'Members' },
{ value: 'media', name: 'Media' },
];
#onTypeChange(event: UUISelectEvent) {
this.type = event.target.value as StartNode['type'];
// Clear others
this.nodeId = '';
this.dynamicPath = '';
this.dispatchEvent(new UmbChangeEvent());
}
#onIdChange(event: CustomEvent) {
this.nodeId = (event.target as UmbInputDocumentElement | UmbInputMediaElement).selectedIds.join('');
this.dispatchEvent(new CustomEvent('change'));
}
render() {
return html`<umb-input-dropdown-list
.options=${this._options}
@change="${this.#onTypeChange}"></umb-input-dropdown-list>
${this.#renderType()}`;
}
#renderType() {
switch (this.type) {
case 'content':
return this.#renderTypeContent();
case 'media':
return this.#renderTypeMedia();
case 'member':
return this.#renderTypeMember();
default:
return 'No type found';
}
}
#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> `;
}
#renderTypeMedia() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => MediaTypes
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> `;
}
static styles = [
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-4);
}
`,
];
}
export default UmbInputContentTypeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-content-type': UmbInputContentTypeElement;
}
}

View File

@@ -1,3 +1,4 @@
export * from './content-type-container-structure-helper.class.js';
export * from './content-type-property-structure-helper.class.js';
export * from './content-type-structure-manager.class.js';
export * from './components/index.js';

View File

@@ -1,6 +1,6 @@
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
export interface UmbPropertyEditorUiElement extends HTMLElement {
value: unknown;
value?: unknown;
config?: UmbPropertyEditorConfigCollection;
}

View File

@@ -1,7 +1,8 @@
import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { StartNode, UmbInputContentTypeElement } from '@umbraco-cms/backoffice/content-type';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
/**
@@ -9,14 +10,28 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
*/
@customElement('umb-property-editor-ui-tree-picker-start-node')
export class UmbPropertyEditorUITreePickerStartNodeElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property()
value = '';
@property({ type: Object })
value?: StartNode;
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
#onChange(event: CustomEvent) {
const target = event.target as UmbInputContentTypeElement;
this.value = {
type: target.type,
id: target.nodeId,
query: target.dynamicPath,
};
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`<div>umb-property-editor-ui-tree-picker-start-node</div>`;
return html`<umb-input-content-type
@change="${this.#onChange}"
.type=${this.value?.type}></umb-input-content-type>`;
}
static styles = [UmbTextStyles];

View File

@@ -15,7 +15,7 @@ const manifest: ManifestPropertyEditorUi = {
properties: [
{
alias: 'startNode',
label: 'Start node',
label: 'Node type',
description: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker.StartNode',
},

View File

@@ -1,24 +1,74 @@
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { StartNode } from '@umbraco-cms/backoffice/content-type';
import { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree';
/**
* @element umb-property-editor-ui-tree-picker
*/
@customElement('umb-property-editor-ui-tree-picker')
export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property()
value = '';
@property({ attribute: false })
public config?: UmbPropertyEditorConfigCollection;
@state()
type?: StartNode['type'];
render() {
return html`<div>umb-property-editor-ui-tree-picker</div>`;
@state()
startNodeId?: string | null;
@state()
min = 0;
@state()
max = 0;
@state()
filter?: string | null;
@state()
showOpenButton?: boolean;
@state()
ignoreUserStartNodes?: boolean;
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
const startNode: StartNode | undefined = config?.getValueByAlias('startNode');
if (startNode) {
this.type = startNode.type;
this.startNodeId = startNode.id;
}
this.min = config?.getValueByAlias('minNumber') || 0;
this.max = config?.getValueByAlias('maxNumber') || 0;
this.filter = config?.getValueByAlias('filter');
this.showOpenButton = config?.getValueByAlias('showOpenButton');
this.ignoreUserStartNodes = config?.getValueByAlias('ignoreUserStartNodes');
}
#onChange(e: CustomEvent) {
this.value = (e.target as UmbInputTreeElement).value as string;
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`<umb-input-tree
.value=${this.value}
.type=${this.type}
.startNodeId=${this.startNodeId ?? ''}
.min=${this.min}
.max=${this.max}
.filter=${this.filter ?? ''}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}></umb-input-tree>`;
}
static styles = [UmbTextStyles];
}

View File

@@ -0,0 +1 @@
export * from './input-tree/index.js';

View File

@@ -0,0 +1 @@
export * from './input-tree.element.js';

View File

@@ -0,0 +1,128 @@
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { StartNode } from '@umbraco-cms/backoffice/content-type';
@customElement('umb-input-tree')
export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
protected getFormElement() {
return undefined;
}
private _type: StartNode['type'] = undefined;
@property()
public set type(newType: StartNode['type']) {
const oldType = this._type;
if (newType?.toLowerCase() !== this._type) {
this._type = newType?.toLowerCase() as StartNode['type'];
this.requestUpdate('type', oldType);
}
}
public get type(): StartNode['type'] {
return this._type;
}
@property({ type: String })
startNodeId?: string;
@property({ type: Number })
min = 0;
@property({ type: Number })
max = 0;
private _filter: Array<string> = [];
@property()
public set filter(value: string) {
this._filter = value.split(',');
}
public get filter(): string {
return this._filter.join(',');
}
@property({ type: Boolean })
showOpenButton?: boolean;
@property({ type: Boolean })
ignoreUserStartNodes?: boolean;
@property()
public set value(newValue: string) {
super.value = newValue;
if (newValue) {
this.selectedIds = newValue.split(',');
} else {
this.selectedIds = [];
}
}
public get value(): string {
return super.value as string;
}
selectedIds: Array<string> = [];
#onChange(event: CustomEvent) {
this.value = (event.target as UmbInputDocumentElement).selectedIds.join(',');
this.dispatchEvent(new UmbChangeEvent());
}
constructor() {
super();
}
render() {
switch (this._type) {
case 'content':
return html`<umb-input-document
.selectedIds=${this.selectedIds}
.startNodeId=${this.startNodeId}
.filter=${this.filter}
.min=${this.min}
.max=${this.max}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}></umb-input-document>`;
case 'media':
return html`<umb-input-media
.selectedIds=${this.selectedIds}
.startNodeId=${this.startNodeId}
.filter=${this.filter}
.min=${this.min}
.max=${this.max}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}></umb-input-media>`;
case 'member':
return html`<umb-input-member
.selectedIds=${this.selectedIds}
.filter=${this.filter}
.min=${this.min}
.max=${this.max}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}>
</umb-input-member>`;
default:
return html`Type could not be found`;
}
}
static styles = [
css`
p {
margin: 0;
color: var(--uui-color-border-emphasis);
}
`,
];
}
export default UmbInputTreeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-tree': UmbInputTreeElement;
}
}

View File

@@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/web-components';
import './input-tree.element.js';
import type { UmbInputTreeElement } from './input-tree.element.js';
const meta: Meta<UmbInputTreeElement> = {
title: 'Components/Inputs/Tree',
component: 'umb-input-tree',
};
export default meta;
type Story = StoryObj<UmbInputTreeElement>;
export const Overview: Story = {
args: {},
};

View File

@@ -0,0 +1,18 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbInputTreeElement } from './input-tree.element.js';
import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
describe('UmbInputTreeElement', () => {
let element: UmbInputTreeElement;
beforeEach(async () => {
element = await fixture(html` <umb-input-tree></umb-input-tree> `);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbInputTreeElement);
});
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
});
});

View File

@@ -5,6 +5,7 @@ export * from './tree-item-base/index.js';
export * from './tree-menu-item/index.js';
export * from './tree.context.js';
export * from './tree.element.js';
export * from './components/index.js';
export interface UmbTreeRootModel {
type: string;

View File

@@ -95,19 +95,28 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
render() {
return html`
${this._items ? html`
<uui-ref-list>${
repeat(this._items,
(item) => item.id,
(item) => this._renderItem(item))}
</uui-ref-list>`: ''
}
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open"
>Add</uui-button
>
${this._items
? html` <uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this._renderItem(item),
)}
</uui-ref-list>`
: ''}
${this.#renderAddButton()}
`;
}
#renderAddButton() {
if (this.max > 0 && this.selectedIds.length >= this.max) return;
return html`<uui-button
id="add-button"
look="placeholder"
@click=${() => this.#pickerContext.openPicker()}
label=${this.localize.term('general_add')}></uui-button>`;
}
private _renderItem(item: DocumentItemResponseModel) {
if (!item.id) return;
return html`