Merge branch 'main' into feature/remove-parent-from-detail-model

This commit is contained in:
Mads Rasmussen
2024-03-01 16:01:04 +01:00
174 changed files with 1299 additions and 874 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@umbraco-cms/backoffice",
"version": "14.0.0--preview008",
"version": "14.0.0--beta001",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "14.0.0--preview008",
"version": "14.0.0--beta001",
"license": "MIT",
"dependencies": {
"@openid/appauth": "^1.3.1",

View File

@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "14.0.0--preview008",
"version": "14.0.0--beta001",
"type": "module",
"exports": {
".": null,

View File

@@ -244,9 +244,8 @@ export abstract class UmbBaseExtensionInitializer<
const newPermission = await this._conditionsAreGood();
// Only set new permission if we are still positive, otherwise it means that we have been destroyed in the mean time.
if (newPermission === false || this._isConditionsPositive === false) {
console.warn(
'If this happens then please inform Niels Lyngsø on CMS Team. We are still investigating wether this is a situation we should handle. Ref. No.: 1.',
);
// Then we need to revert the above work:
this._conditionsAreBad();
return;
}
// We update the oldValue as this point, cause in this way we are sure its the value at this point, when doing async code someone else might have changed the state in the mean time.
@@ -259,9 +258,6 @@ export abstract class UmbBaseExtensionInitializer<
// Only continue if we are still negative, otherwise it means that something changed in the mean time.
if (this._isConditionsPositive === true) {
console.warn(
'If this happens then please inform Niels Lyngsø on CMS Team. We are still investigating wether this is a situation we should handle. Ref. No.: 2.',
);
return;
}
// We update the oldValue as this point, cause in this way we are sure its the value at this point, when doing async code someone else might have changed the state in the mean time.

View File

@@ -1,3 +1,4 @@
import { pagedResult } from '../paged-result.js';
import type { UmbEntityMockDbBase } from './entity-base.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -11,18 +12,18 @@ export class UmbMockEntityTreeManager<T extends Omit<EntityTreeItemResponseModel
this.#treeItemMapper = treeItemMapper;
}
getRoot() {
getRoot({ skip = 0, take = 100 }: { skip?: number; take?: number } = {}) {
const items = this.#db.getAll().filter((item) => item.parent === null);
const treeItems = items.map((item) => this.#treeItemMapper(item));
const total = items.length;
return { items: treeItems, total };
const paged = pagedResult(items, skip, take);
const treeItems = paged.items.map((item) => this.#treeItemMapper(item));
return { items: treeItems, total: paged.total };
}
getChildrenOf(parentId: string) {
getChildrenOf({ parentId, skip = 0, take = 100 }: { parentId: string; skip?: number; take?: number }) {
const items = this.#db.getAll().filter((item) => item.parent?.id === parentId);
const treeItems = items.map((item) => this.#treeItemMapper(item));
const total = items.length;
return { items: treeItems, total };
const paged = pagedResult(items, skip, take);
const treeItems = paged.items.map((item) => this.#treeItemMapper(item));
return { items: treeItems, total: paged.total };
}
move(ids: Array<string>, destinationId: string) {

View File

@@ -1,5 +1,6 @@
import type { UmbMockDBBase } from '../mock-db-base.js';
import { createFileSystemTreeItem } from '../../utils.js';
import { pagedResult } from '../paged-result.js';
import type { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
export class UmbMockFileSystemTreeManager<T extends Omit<FileSystemTreeItemPresentationModel, 'type'>> {
@@ -9,20 +10,23 @@ export class UmbMockFileSystemTreeManager<T extends Omit<FileSystemTreeItemPrese
this.#db = mockDb;
}
getRoot(): { items: Array<Omit<FileSystemTreeItemPresentationModel, 'type'>>; total: number } {
getRoot({ skip = 0, take = 100 }: { skip?: number; take?: number } = {}): {
items: Array<Omit<FileSystemTreeItemPresentationModel, 'type'>>;
total: number;
} {
const items = this.#db.getAll().filter((item) => item.parent === null);
const treeItems = items.map((item) => createFileSystemTreeItem(item));
const total = items.length;
return { items: treeItems, total };
const paged = pagedResult(items, skip, take);
const treeItems = paged.items.map((item) => createFileSystemTreeItem(item));
return { items: treeItems, total: paged.total };
}
getChildrenOf(parentPath: string): {
getChildrenOf({ parentPath, skip = 0, take = 100 }: { parentPath: string; skip?: number; take?: number }): {
items: Array<Omit<FileSystemTreeItemPresentationModel, 'type'>>;
total: number;
} {
const items = this.#db.getAll().filter((item) => item.parent?.path === parentPath);
const treeItems = items.map((item) => createFileSystemTreeItem(item));
const total = items.length;
return { items: treeItems, total };
const paged = pagedResult(items, skip, take);
const treeItems = paged.items.map((item) => createFileSystemTreeItem(item));
return { items: treeItems, total: paged.total };
}
}

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbDataTypeMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDataTypeMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbDataTypeMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDataTypeMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbDictionaryMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDictionaryMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbDictionaryMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDictionaryMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbDocumentTypeMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentTypeMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbDocumentTypeMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentTypeMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const recycleBinHandlers = [
rest.get(umbracoPath(`/recycle-bin${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbDocumentMockDb.recycleBin.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentMockDb.recycleBin.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/recycle-bin${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbDocumentMockDb.recycleBin.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentMockDb.recycleBin.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbDocumentMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbDocumentMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbDocumentMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbMediaTypeMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaTypeMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbMediaTypeMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaTypeMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const recycleBinHandlers = [
rest.get(umbracoPath(`/recycle-bin${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbMediaMockDb.recycleBin.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaMockDb.recycleBin.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/recycle-bin${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbMediaMockDb.recycleBin.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaMockDb.recycleBin.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbMediaMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbMediaMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMediaMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbMemberTypeMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMemberTypeMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbMemberTypeMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbMemberTypeMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbPartialViewMockDB.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbPartialViewMockDB.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentPath = req.url.searchParams.get('parentPath');
if (!parentPath) return res(ctx.status(400));
const response = umbPartialViewMockDB.tree.getChildrenOf(parentPath);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbPartialViewMockDB.tree.getChildrenOf({ parentPath, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbScriptMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbScriptMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentPath = req.url.searchParams.get('parentPath');
if (!parentPath) return res(ctx.status(400));
const response = umbScriptMockDb.tree.getChildrenOf(parentPath);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbScriptMockDb.tree.getChildrenOf({ parentPath, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbStaticFileMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbStaticFileMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentPath = req.url.searchParams.get('parentPath');
if (!parentPath) return res(ctx.status(400));
const response = umbStaticFileMockDb.tree.getChildrenOf(parentPath);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbStaticFileMockDb.tree.getChildrenOf({ parentPath, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbStylesheetMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbStylesheetMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentPath = req.url.searchParams.get('parentPath');
if (!parentPath) return res(ctx.status(400));
const response = umbStylesheetMockDb.tree.getChildrenOf(parentPath);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbStylesheetMockDb.tree.getChildrenOf({ parentPath, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -5,14 +5,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const treeHandlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const response = umbTemplateMockDb.tree.getRoot();
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbTemplateMockDb.tree.getRoot({ skip, take });
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const response = umbTemplateMockDb.tree.getChildrenOf(parentId);
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const response = umbTemplateMockDb.tree.getChildrenOf({ parentId, skip, take });
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -119,7 +119,8 @@ export class UmbBlockGridAreaTypeWorkspaceContext
context.setValue(appendToFrozenArray(context.getValue() ?? [], this.#data.getValue(), (x) => x?.key));
});
this.saveComplete(this.#data.value);
this.setIsNew(false);
this.workspaceComplete(this.#data.value);
}
public destroy(): void {

View File

@@ -14,6 +14,11 @@ export const manifest: ManifestPropertyEditorUi = {
group: 'richContent',
settings: {
properties: [
{
alias: 'blockGroups',
label: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockTypeGroupConfiguration',
},
{
alias: 'useLiveEditing',
label: 'Live editing mode',

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
js: () => import('./property-editor-ui-block-grid-type-configuration.element.js'),
meta: {
label: 'Block Grid Block Configuration',
propertyEditorSchemaAlias: 'Umbraco.BlockGrid.BlockConfiguration',
icon: 'icon-autofill',
group: 'blocks',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
js: () => import('./property-editor-ui-block-list-type-configuration.element.js'),
meta: {
label: 'Block List Type Configuration',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'common',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
js: () => import('./property-editor-ui-block-type-group-configuration.element.js'),
meta: {
label: 'Block Grid Group Configuration',
propertyEditorSchemaAlias: 'Umbraco.BlockGrid.GroupConfiguration',
icon: 'icon-autofill',
group: 'blocks',
},

View File

@@ -121,7 +121,8 @@ export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeWith
);
});
this.saveComplete(this.#data.value);
this.setIsNew(false);
this.workspaceComplete(this.#data.value);
}
public destroy(): void {

View File

@@ -295,7 +295,8 @@ export class UmbBlockWorkspaceContext<
}
}
this.saveComplete(layoutData);
this.setIsNew(false);
this.workspaceComplete(layoutData);
}
#modalRejected = () => {

View File

@@ -8,7 +8,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-collection-pagination')
export class UmbCollectionPaginationElement extends UmbLitElement {
@state()
_totalPages = 0;
_totalPages = 1;
@state()
_currentPage = 1;

View File

@@ -1,5 +1,6 @@
import { UMB_DEFAULT_COLLECTION_CONTEXT, UmbDefaultCollectionContext } from './collection-default.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -35,6 +36,11 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
});
}
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.firstUpdated(_changedProperties);
this.#collectionContext?.requestCollection();
}
#observeCollectionRoutes() {
if (!this.#collectionContext) return;

View File

@@ -1,4 +1,4 @@
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { 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';
@@ -17,25 +17,27 @@ export class UmbInputNumberRangeElement extends FormControlMixin(UmbLitElement)
@state()
private _minValue?: number;
@property()
public get minValue() {
return this._minValue;
}
@property({ type: Number })
public set minValue(value: number | undefined) {
this._minValue = value;
this.updateValue();
}
public get minValue() {
return this._minValue;
}
@state()
private _maxValue?: number;
@property()
public get maxValue() {
return this._maxValue;
}
@property({ type: Number })
public set maxValue(value: number | undefined) {
this._maxValue = value;
this.updateValue();
}
public get maxValue() {
return this._maxValue;
}
private updateValue() {
const newValue = this._minValue || this._maxValue ? (this._minValue || '') + ',' + (this._maxValue || '') : '';

View File

@@ -1,32 +1,21 @@
import type { UmbMultipleColorPickerItemInputElement } from './multiple-color-picker-item-input.element.js';
import type { UmbSwatchDetails } from '@umbraco-cms/backoffice/models';
import {
css,
customElement,
html,
ifDefined,
nothing,
repeat,
customElement,
property,
state,
ifDefined,
} from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { type UmbInputEvent, UmbChangeEvent, type UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails, UmbMultipleColorPickerItemInputElement> = {
getUniqueOfElement: (element) => {
return element.value.toString();
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.value;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-multiple-color-picker-item-input',
containerSelector: '#sorter-wrapper',
};
import type { UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import type { UmbSwatchDetails } from '@umbraco-cms/backoffice/models';
/**
* @element umb-multiple-color-picker-input
@@ -34,7 +23,15 @@ const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails, UmbMultipleColorPickerIte
@customElement('umb-multiple-color-picker-input')
export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitElement) {
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
getUniqueOfElement: (element: UmbMultipleColorPickerItemInputElement) => {
return element.value.toString();
},
getUniqueOfModel: (modelEntry: UmbSwatchDetails) => {
return modelEntry.value;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-multiple-color-picker-item-input',
containerSelector: '#sorter-wrapper',
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
@@ -194,7 +191,6 @@ export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitE
?showLabels=${this.showLabels}
value=${item.value}
label=${ifDefined(item.label)}
name="item-${index}"
@change=${(event: UmbChangeEvent) => this.#onChange(event, index)}
@delete="${(event: UmbDeleteEvent) => this.#deleteItem(event, index)}"
?disabled=${this.disabled}

View File

@@ -1,37 +1,26 @@
import type { UmbInputMultipleTextStringItemElement } from './input-multiple-text-string-item.element.js';
import { css, html, nothing, repeat, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
export type MultipleTextStringValue = Array<MultipleTextStringValueItem>;
export interface MultipleTextStringValueItem {
value: string;
}
const SORTER_CONFIG: UmbSorterConfig<MultipleTextStringValueItem> = {
getUniqueOfElement: (element) => {
return element.getAttribute('data-sort-entry-id');
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.value;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-input-multiple-text-string-item',
containerSelector: '#sorter-wrapper',
};
import type { UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
/**
* @element umb-input-multiple-text-string
*/
@customElement('umb-input-multiple-text-string')
export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitElement) {
#prevalueSorter = new UmbSorterController(this, {
...SORTER_CONFIG,
#sorter = new UmbSorterController(this, {
getUniqueOfElement: (element) => {
return element.getAttribute('data-sort-entry-id');
},
getUniqueOfModel: (modelEntry: string) => {
return modelEntry;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-input-multiple-text-string-item',
containerSelector: '#sorter-wrapper',
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
@@ -120,17 +109,17 @@ export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitEl
}
@state()
private _items: MultipleTextStringValue = [];
private _items: Array<string> = [];
@property({ type: Array })
public get items(): MultipleTextStringValue {
public get items(): Array<string> {
return this._items;
}
public set items(items: MultipleTextStringValue) {
public set items(items: Array<string>) {
// TODO: when we have a way to overwrite the missing value validator we can remove this
this.value = items?.length > 0 ? 'some value' : '';
this._items = items ?? [];
this.#prevalueSorter.setModel(this.items);
this.#sorter.setModel(this.items);
}
// TODO: Some inputs might not have a value that is either FormDataEntryValue or FormData.
@@ -146,7 +135,7 @@ export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitEl
*/
#onAdd() {
this._items = [...this._items, { value: '' }];
this._items = [...this._items, ''];
this.pristine = false;
this.dispatchEvent(new UmbChangeEvent());
this.#focusNewItem();
@@ -156,7 +145,7 @@ export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitEl
event.stopPropagation();
const target = event.currentTarget as UmbInputMultipleTextStringItemElement;
const value = target.value as string;
this._items = this._items.map((item, index) => (index === currentIndex ? { value } : item));
this._items = this._items.map((item, index) => (index === currentIndex ? value : item));
this.dispatchEvent(new UmbChangeEvent());
}
@@ -192,9 +181,9 @@ export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitEl
(item, index) => index,
(item, index) =>
html` <umb-input-multiple-text-string-item
value=${item.value}
value=${item}
name="item-${index}"
data-sort-entry-id=${item.value}
data-sort-entry-id=${item}
@input=${(event: UmbInputEvent) => this.#onInput(event, index)}
@delete="${(event: UmbDeleteEvent) => this.#deleteItem(event, index)}"
?disabled=${this.disabled}

View File

@@ -18,6 +18,7 @@ export class UmbDeleteEntityAction<
color: 'danger',
confirmLabel: 'Delete',
});
await this.repository?.delete(this.unique);
}
}

View File

@@ -27,6 +27,7 @@ export class UmbExtensionCollectionElement extends UmbCollectionDefaultElement {
});
}
// TODO: make this a utility function, please check that we do not already have on for this: [NL]
// credit: https://stackoverflow.com/a/7225450/12787 [LK]
#camelCaseToWords(input: string) {
const result = input.replace(/([A-Z])/g, ' $1');
@@ -35,7 +36,6 @@ export class UmbExtensionCollectionElement extends UmbCollectionDefaultElement {
#onChange(event: UUISelectEvent) {
const extensionType = event.target.value;
console.log('onChange', extensionType);
this.#collectionContext?.setFilter({ type: extensionType });
}

View File

@@ -0,0 +1,88 @@
import { umbExtensionsRegistry } from './registry.js';
import { html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionElementInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
// TODO: Eslint: allow abstract element class to end with "ElementBase" instead of "Element"
// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name
export abstract class UmbExtensionInitializerElementBase<
ManifestType extends ManifestWithDynamicConditions,
> extends UmbLitElement {
_alias?: string;
@property({ type: String, reflect: true })
get alias() {
return this._alias;
}
set alias(newVal) {
this._alias = newVal;
this.#observeManifest();
}
@property({ type: Object, attribute: false })
get props() {
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.#extensionElementController) {
this.#extensionElementController.properties = newVal;
}
}
#props?: Record<string, unknown> = {};
#extensionElementController?: UmbExtensionElementInitializer<ManifestType>;
@state()
_element: HTMLElement | undefined;
abstract getExtensionType(): string;
abstract getDefaultElementName(): string;
#observeManifest() {
if (!this._alias) return;
this.observe(
umbExtensionsRegistry.byTypeAndAlias(this.getExtensionType(), this._alias),
async (m) => {
if (!m) return;
const manifest = m as unknown as ManifestType;
this.createApi(manifest);
this.createElement(manifest);
},
'umbObserveTreeManifest',
);
}
protected async createApi(manifest?: ManifestType) {
if (!manifest) throw new Error('No manifest');
const api = (await createExtensionApi(manifest, [this])) as unknown as any;
if (!api) throw new Error('No api');
api.setManifest(manifest);
}
protected async createElement(manifest?: ManifestType) {
if (!manifest) throw new Error('No manifest');
const extController = new UmbExtensionElementInitializer<ManifestType>(
this,
umbExtensionsRegistry,
manifest.alias,
this.#extensionChanged,
this.getDefaultElementName(),
);
extController.properties = this.#props;
this.#extensionElementController = extController;
}
#extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementInitializer<ManifestType>) => {
this._element = isPermitted ? controller.component : undefined;
this.requestUpdate('_element');
};
render() {
return html`${this._element}`;
}
}

View File

@@ -2,3 +2,5 @@ export * from './conditions/index.js';
export * from './interfaces/index.js';
export * from './models/index.js';
export * from './registry.js';
export { UmbExtensionInitializerElementBase } from './extension-initializer-element-base.js';

View File

@@ -6,5 +6,4 @@ export * from './property-editor-ui-element.interface.js';
export * from './section-element.interface.js';
export * from './section-sidebar-app-element.interface.js';
export * from './section-view-element.interface.js';
export * from './tree-item-element.interface.js';
export * from './workspace-view-element.interface.js';

View File

@@ -1,5 +0,0 @@
import type { UmbTreeItemModelBase } from '@umbraco-cms/backoffice/tree';
export interface UmbTreeItemElement extends HTMLElement {
item?: UmbTreeItemModelBase;
}

View File

@@ -1,7 +1,7 @@
import type { UmbTreeItemElement } from '../interfaces/index.js';
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
import type { UmbTreeItemContext, UmbTreeItemModelBase } from '../../index.js';
import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestTreeItem extends ManifestElement<UmbTreeItemElement> {
export interface ManifestTreeItem extends ManifestElementAndApi<HTMLElement, UmbTreeItemContext<UmbTreeItemModelBase>> {
type: 'treeItem';
meta: MetaTreeItem;
}

View File

@@ -1,6 +1,6 @@
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestTree extends ManifestBase {
export interface ManifestTree extends ManifestElementAndApi {
type: 'tree';
meta: MetaTree;
}

View File

@@ -13,9 +13,11 @@ export class UmbConfirmModalController extends UmbControllerBase {
data: args,
});
await modalContext.onSubmit().catch(() => {
const p = modalContext.onSubmit();
p.catch(() => {
this.destroy();
});
await p;
// This is a one time off, so we can destroy our selfs.
this.destroy();

View File

@@ -98,6 +98,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
const linkType = (entityType as UmbLinkPickerLinkType) ?? 'external';
this._selectedKey = selectedKey;
if (this._selectedKey === undefined) return;
this._selectionConfiguration.selection = [this._selectedKey];
this.#partialUpdateLink({ type: linkType, unique: selectedKey, url: selectedKey });
this.requestUpdate();
@@ -187,10 +188,12 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
placeholder=${this.localize.term('placeholders_search')}
label=${this.localize.term('placeholders_search')}></uui-input>
<umb-tree
?hide-tree-root=${true}
alias=${UMB_DOCUMENT_TREE_ALIAS}
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'document')}
.selectionConfiguration=${this._selectionConfiguration}></umb-tree>
.props=${{
hideTreeRoot: true,
selectionConfiguration: this._selectionConfiguration,
}}
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'document')}></umb-tree>
</div>
<hr />
<uui-symbol-expand
@@ -200,10 +203,12 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<uui-label for="media-expand">${this.localize.term('defaultdialogs_linkToMedia')}</uui-label>
<div style="${styleMap({ display: !this.mediaExpanded ? 'block' : 'none' })}">
<umb-tree
?hide-tree-root=${true}
alias="Umb.Tree.Media"
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'media')}
.selectionConfiguration=${this._selectionConfiguration}></umb-tree>
.props=${{
hideTreeRoot: true,
selectionConfiguration: this._selectionConfiguration,
}}
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'media')}></umb-tree>
</div>
`;
}

View File

@@ -43,12 +43,6 @@ const modals: Array<ManifestModal> = [
name: 'Embedded Media Modal',
js: () => import('./embedded-media/embedded-media-modal.element.js'),
},
{
type: 'modal',
alias: 'Umb.Modal.TreePicker',
name: 'Tree Picker Modal',
js: () => import('./tree-picker/tree-picker-modal.element.js'),
},
];
export const manifests = [...modals];

View File

@@ -6,7 +6,7 @@ import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/doc
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIBooleanInputEvent, UUIInputEvent, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, nothing, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertySettingsModalValue, UmbPropertySettingsModalData } from '@umbraco-cms/backoffice/modal';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { generateAlias } from '@umbraco-cms/backoffice/utils';
@@ -234,6 +234,7 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement<
<uui-input
id="name-input"
name="name"
label="property name (TODO: Localize)"
@input=${this.#onNameChange}
.value=${this.value.name}
placeholder="Enter a name...">
@@ -259,7 +260,7 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement<
.value=${this.value.description}></uui-textarea>
</div>
<umb-data-type-flow-input
.value=${this.value.dataType?.unique}
.value=${ifDefined(this.value.dataType?.unique)}
@change=${this.#onDataTypeIdChange}></umb-data-type-flow-input>
<hr />
<div class="container">

View File

@@ -14,11 +14,6 @@ export const manifest: ManifestPropertyEditorSchema = {
description: 'Define Blocks based on Element Types.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockGridTypeConfiguration',
},
{
alias: 'blockGroups',
label: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockTypeGroupConfiguration',
},
{
alias: 'validationLimit',
label: 'Amount',

View File

@@ -7,5 +7,19 @@ export const manifest: ManifestPropertyEditorSchema = {
alias: 'Umbraco.DropDown.Flexible',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Dropdown',
settings: {
properties: [
{
alias: 'multiple',
label: 'Enable multiple choice',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
},
{
alias: 'items',
label: 'Add options',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
},
],
},
},
};

View File

@@ -6,5 +6,15 @@ export const manifest: ManifestPropertyEditorSchema = {
alias: 'Umbraco.RadioButtonList',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.RadioButtonList',
settings: {
properties: [
{
alias: 'items',
label: 'Add option',
description: 'Add, remove or sort options for the list.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
},
],
},
},
};

View File

@@ -12,13 +12,20 @@ export const manifest: ManifestPropertyEditorSchema = {
alias: 'mediaParentId',
label: 'Image Upload Folder',
description: 'Choose the upload location of pasted images',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaPicker',
config: [{ alias: 'validationLimit', value: { min: 0, max: 1 } }],
},
{
alias: 'ignoreUserStartNodes',
label: 'Ignore User Start Nodes',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
},
{
alias: 'blocks',
label: 'Available Blocks',
description: 'Define the available blocks.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockRteTypeConfiguration',
},
],
},
},

View File

@@ -6,6 +6,6 @@ export const manifest: ManifestPropertyEditorSchema = {
name: 'Date/Time',
alias: 'Umbraco.TrueFalse',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TrueFalse',
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
},
};

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-collection-view-bulk-action-permissions.element.js'),
meta: {
label: 'Collection View Bulk Action Permissions',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'lists',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-collection-view-column-configuration.element.js'),
meta: {
label: 'Collection View Column Configuration',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'lists',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-collection-view-layout-configuration.element.js'),
meta: {
label: 'Collection View Layout Configuration',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'lists',
},

View File

@@ -26,7 +26,7 @@ export class UmbPropertyEditorUICollectionViewLayoutConfigurationElement
implements UmbPropertyEditorUiElement
{
@property({ type: Array })
value: Array<LayoutConfig> = [];
value?: Array<LayoutConfig>;
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
@@ -41,47 +41,47 @@ export class UmbPropertyEditorUICollectionViewLayoutConfigurationElement
}
#onAdd() {
this.value = [...this.value, { isSystem: false, icon: 'icon-stop', selected: true }];
this.value = [...(this.value ?? []), { isSystem: false, icon: 'icon-stop', selected: true }];
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
#onRemove(unique: number) {
const values = [...this.value];
const values = [...(this.value ?? [])];
values.splice(unique, 1);
this.value = values;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
#onChangePath(e: UUIInputEvent, index: number) {
const values = [...this.value];
const values = [...(this.value ?? [])];
values[index] = { ...values[index], path: e.target.value as string };
this.value = values;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
#onChangeName(e: UUIInputEvent, index: number) {
const values = [...this.value];
const values = [...(this.value ?? [])];
values[index] = { ...values[index], name: e.target.value as string };
this.value = values;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
#onChangeSelected(e: UUIBooleanInputEvent, index: number) {
const values = [...this.value];
const values = [...(this.value ?? [])];
values[index] = { ...values[index], selected: e.target.checked };
this.value = values;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
async #onIconChange(index: number) {
const icon = this.#iconReader(this.value[index].icon ?? '');
const icon = this.#iconReader((this.value ? this.value[index].icon : undefined) ?? '');
// TODO: send icon data to modal
const modalContext = this._modalContext?.open(UMB_ICON_PICKER_MODAL);
const picked = await modalContext?.onSubmit();
if (!picked) return;
const values = [...this.value];
const values = [...(this.value ?? [])];
values[index] = { ...values[index], icon: `${picked.icon} color-${picked.color}` };
this.value = values;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
@@ -89,16 +89,18 @@ export class UmbPropertyEditorUICollectionViewLayoutConfigurationElement
render() {
return html`<div id="layout-wrapper">
${repeat(
this.value,
(layout, index) => '' + layout.name + layout.icon,
(layout, index) =>
html` <div class="layout-item">
<uui-icon name="icon-navigation"></uui-icon> ${layout.isSystem
? this.renderSystemFieldRow(layout, index)
: this.renderCustomFieldRow(layout, index)}
</div>`,
)}
${this.value
? repeat(
this.value,
(layout, index) => '' + layout.name + layout.icon,
(layout, index) =>
html` <div class="layout-item">
<uui-icon name="icon-navigation"></uui-icon> ${layout.isSystem
? this.renderSystemFieldRow(layout, index)
: this.renderCustomFieldRow(layout, index)}
</div>`,
)
: ''}
</div>
<uui-button
id="add"

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-collection-view-order-by.element.js'),
meta: {
label: 'Collection View Order By',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'lists',
},

View File

@@ -21,10 +21,6 @@ export class UmbPropertyEditorUIColorSwatchesEditorElement extends UmbLitElement
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
this._showLabels = config?.getValueByAlias('useLabel') ?? this.#defaultShowLabels;
const items = config?.getValueByAlias('items') as typeof this.value;
if (items) {
this.value = items;
}
}
#onChange(event: CustomEvent) {

View File

@@ -10,19 +10,5 @@ export const manifest: ManifestPropertyEditorUi = {
propertyEditorSchemaAlias: 'Umbraco.DropDown.Flexible',
icon: 'icon-time',
group: 'pickers',
settings: {
properties: [
{
alias: 'multiple',
label: 'Enable multiple choice',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
},
{
alias: 'items',
label: 'Add options',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
},
],
},
},
};

View File

@@ -4,7 +4,7 @@ import { html, customElement, property, state, ifDefined } from '@umbraco-cms/ba
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { MultipleTextStringValue, UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components';
import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components';
/**
* @element umb-property-editor-ui-multiple-text-string
@@ -12,7 +12,7 @@ import type { MultipleTextStringValue, UmbInputMultipleTextStringElement } from
@customElement('umb-property-editor-ui-multiple-text-string')
export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Array })
public value: MultipleTextStringValue = [];
public value: Array<string> = [];
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-number-range.element.js'),
meta: {
label: 'Number Range',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'common',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-order-direction.element.js'),
meta: {
label: 'Order Direction',
propertyEditorSchemaAlias: '',
icon: 'icon-autofill',
group: 'common',
},

View File

@@ -7,7 +7,6 @@ export const manifest: ManifestPropertyEditorUi = {
element: () => import('./property-editor-ui-overlay-size.element.js'),
meta: {
label: 'Overlay Size',
propertyEditorSchemaAlias: '',
icon: 'icon-document',
group: '',
},

View File

@@ -10,15 +10,5 @@ export const manifest: ManifestPropertyEditorUi = {
propertyEditorSchemaAlias: 'Umbraco.RadioButtonList',
icon: 'icon-target',
group: 'lists',
settings: {
properties: [
{
alias: 'items',
label: 'Add option',
description: 'Add, remove or sort options for the list.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
},
],
},
},
};

View File

@@ -9,6 +9,5 @@ export const manifest: ManifestPropertyEditorUi = {
label: 'Tree Picker Source Picker',
icon: 'icon-page-add',
group: 'pickers',
propertyEditorSchemaAlias: '',
},
};

View File

@@ -9,6 +9,5 @@ export const manifest: ManifestPropertyEditorUi = {
label: 'Tree Picker Source Type Picker',
icon: 'icon-page-add',
group: 'pickers',
propertyEditorSchemaAlias: '',
},
};

View File

@@ -110,12 +110,21 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
public setAlias(alias: string | undefined): void {
this.#alias.setValue(alias);
}
public getAlias(): string | undefined {
return this.#alias.getValue();
}
public setLabel(label: string | undefined): void {
this.#label.setValue(label);
}
public getLabel(): string | undefined {
return this.#label.getValue();
}
public setDescription(description: string | undefined): void {
this.#description.setValue(description);
}
public getDescription(): string | undefined {
return this.#description.getValue();
}
/**
* Set the value of this property.
* @param value {ValueType} the whole value to be set
@@ -136,6 +145,9 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
public setConfig(config: Array<UmbPropertyEditorConfigProperty> | undefined): void {
this.#configValues.setValue(config ?? []);
}
public getConfig(): Array<UmbPropertyEditorConfigProperty> | undefined {
return this.#configValues.getValue();
}
public setVariantId(variantId: UmbVariantId | undefined): void {
this.#variantId.setValue(variantId);
}

View File

@@ -27,9 +27,12 @@ export class UmbPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String })
public set label(label: string) {
public set label(label: string | undefined) {
this.#propertyContext.setLabel(label);
}
public get label() {
return this.#propertyContext.getLabel();
}
/**
* Description: render a description underneath the label.
@@ -38,9 +41,12 @@ export class UmbPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String })
public set description(description: string) {
public set description(description: string | undefined) {
this.#propertyContext.setDescription(description);
}
public get description() {
return this.#propertyContext.getDescription();
}
/**
* Alias
@@ -53,6 +59,9 @@ export class UmbPropertyElement extends UmbLitElement {
public set alias(alias: string) {
this.#propertyContext.setAlias(alias);
}
public get alias() {
return this.#propertyContext.getAlias() ?? '';
}
/**
* Property Editor UI Alias. Render the Property Editor UI registered for this alias.
@@ -62,12 +71,14 @@ export class UmbPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String, attribute: 'property-editor-ui-alias' })
public set propertyEditorUiAlias(value: string) {
if (this._propertyEditorUiAlias === value) return;
public set propertyEditorUiAlias(value: string | undefined) {
this._propertyEditorUiAlias = value;
this._observePropertyEditorUI();
}
private _propertyEditorUiAlias = '';
public get propertyEditorUiAlias(): string {
return this._propertyEditorUiAlias ?? '';
}
private _propertyEditorUiAlias?: string;
/**
* Config. Configuration to pass to the Property Editor UI. This is also the configuration data stored on the Data Type.
@@ -80,6 +91,9 @@ export class UmbPropertyElement extends UmbLitElement {
public set config(value: UmbPropertyEditorConfig | undefined) {
this.#propertyContext.setConfig(value);
}
public get config(): UmbPropertyEditorConfig | undefined {
return this.#propertyContext.getConfig();
}
@state()
private _variantDifference?: string;
@@ -130,13 +144,15 @@ export class UmbPropertyElement extends UmbLitElement {
};
private _observePropertyEditorUI(): void {
this.observe(
umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', this._propertyEditorUiAlias),
(manifest) => {
this._gotEditorUI(manifest);
},
'_observePropertyEditorUI',
);
if (this._propertyEditorUiAlias) {
this.observe(
umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', this._propertyEditorUiAlias),
(manifest) => {
this._gotEditorUI(manifest);
},
'_observePropertyEditorUI',
);
}
}
private async _gotEditorUI(manifest?: ManifestPropertyEditorUi | null): Promise<void> {

View File

@@ -1,2 +0,0 @@
export type { UmbTreeDataSource } from './tree-data-source.interface.js';
export { UmbTreeServerDataSourceBase } from './tree-server-data-source-base.js';

View File

@@ -0,0 +1,10 @@
export { UmbTreeServerDataSourceBase } from './tree-server-data-source-base.js';
export { UmbTreeRepositoryBase } from './tree-repository-base.js';
export type { UmbTreeDataSource } from './tree-data-source.interface.js';
export type { UmbTreeRepository } from './tree-repository.interface.js';
export type { UmbTreeStore } from './tree-store.interface.js';
export type { UmbTreeRootItemsRequestArgs, UmbTreeChildrenOfRequestArgs } from './types.js';
export { UmbUniqueTreeStore } from './unique-tree-store.js';

View File

@@ -1,4 +1,5 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js';
import type { UmbPagedModel, UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -21,16 +22,15 @@ export interface UmbTreeDataSourceConstructor<TreeItemType extends UmbTreeItemMo
export interface UmbTreeDataSource<TreeItemType extends UmbTreeItemModelBase> {
/**
* Gets the root items of the tree.
* @return {*} {Promise<DataSourceResponse<UmbPagedModel<TreeItemType>>>}
* @return {*} {Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>}
* @memberof UmbTreeDataSource
*/
getRootItems(): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
getRootItems(args: UmbTreeRootItemsRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
/**
* Gets the children of the given parent item.
* @param {(string | null)} parentUnique
* @return {*} {Promise<DataSourceResponse<UmbPagedModel<TreeItemType>>}
* @return {*} {Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>}
* @memberof UmbTreeDataSource
*/
getChildrenOf(parentUnique: string | null): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
getChildrenOf(args: UmbTreeChildrenOfRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
}

View File

@@ -1,7 +1,7 @@
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '../types.js';
import type { UmbTreeStore } from './tree-store.interface.js';
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from './types.js';
import type { UmbTreeRepository } from './tree-repository.interface.js';
import type { UmbTreeDataSource, UmbTreeDataSourceConstructor } from './data-source/tree-data-source.interface.js';
import type { UmbTreeDataSource, UmbTreeDataSourceConstructor } from './tree-data-source.interface.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
@@ -61,10 +61,10 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestRootTreeItems() {
async requestRootTreeItems(args: any) {
await this._init;
const { data, error } = await this.#treeSource.getRootItems();
const { data, error } = await this.#treeSource.getRootItems(args);
if (data) {
this._treeStore!.appendItems(data.items);
@@ -79,17 +79,17 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestTreeItemsOf(parentUnique: string | null) {
if (parentUnique === undefined) throw new Error('Parent unique is missing');
async requestTreeItemsOf(args: any) {
if (args.parentUnique === undefined) throw new Error('Parent unique is missing');
await this._init;
const { data, error } = await this.#treeSource.getChildrenOf(parentUnique);
const { data, error } = await this.#treeSource.getChildrenOf(args);
if (data) {
this._treeStore!.appendItems(data.items);
}
return { data, error, asObservable: () => this._treeStore!.childrenOf(parentUnique) };
return { data, error, asObservable: () => this._treeStore!.childrenOf(args.parentUnique) };
}
/**

View File

@@ -1,4 +1,5 @@
import type { UmbTreeItemModelBase } from './types.js';
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js';
import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
@@ -27,20 +28,21 @@ export interface UmbTreeRepository<
/**
* Requests the root items of the tree.
* @param {UmbTreeRootItemsRequestArgs} args
* @memberof UmbTreeRepository
*/
requestRootTreeItems: () => Promise<{
requestRootTreeItems: (args: UmbTreeRootItemsRequestArgs) => Promise<{
data?: UmbPagedModel<TreeItemType>;
error?: ProblemDetails;
asObservable?: () => Observable<TreeItemType[]>;
}>;
/**
* Requests the items of a item in the tree.
* @param {(string | null)} parentUnique
* Requests the children of the given parent item.
* @param {UmbTreeChildrenOfRequestArgs} args
* @memberof UmbTreeRepository
*/
requestTreeItemsOf: (parentUnique: string | null) => Promise<{
requestTreeItemsOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<{
data?: UmbPagedModel<TreeItemType>;
error?: ProblemDetails;
asObservable?: () => Observable<TreeItemType[]>;

View File

@@ -1,7 +1,8 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeDataSource } from './tree-data-source.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { TreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
@@ -9,8 +10,8 @@ export interface UmbTreeServerDataSourceBaseArgs<
ServerTreeItemType extends TreeItemPresentationModel,
ClientTreeItemType extends UmbTreeItemModelBase,
> {
getRootItems: () => Promise<UmbPagedModel<ServerTreeItemType>>;
getChildrenOf: (parentUnique: string | null) => Promise<UmbPagedModel<ServerTreeItemType>>;
getRootItems: (args: UmbTreeRootItemsRequestArgs) => Promise<UmbPagedModel<ServerTreeItemType>>;
getChildrenOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<UmbPagedModel<ServerTreeItemType>>;
mapper: (item: ServerTreeItemType) => ClientTreeItemType;
}
@@ -44,11 +45,12 @@ export abstract class UmbTreeServerDataSourceBase<
/**
* Fetches the root items for the tree from the server
* @param {UmbTreeRootItemsRequestArgs} args
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
async getRootItems() {
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getRootItems());
async getRootItems(args: UmbTreeRootItemsRequestArgs) {
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getRootItems(args));
if (data) {
const items = data?.items.map((item) => this.#mapper(item));
@@ -60,14 +62,14 @@ export abstract class UmbTreeServerDataSourceBase<
/**
* Fetches the children of a given parent unique from the server
* @param {(string)} parentUnique
* @param {UmbTreeChildrenOfRequestArgs} args
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
async getChildrenOf(parentUnique: string | null) {
if (parentUnique === undefined) throw new Error('Parent unique is missing');
async getChildrenOf(args: UmbTreeChildrenOfRequestArgs) {
if (args.parentUnique === undefined) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getChildrenOf(parentUnique));
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getChildrenOf(args));
if (data) {
const items = data?.items.map((item: ServerTreeItemType) => this.#mapper(item));

View File

@@ -1,4 +1,4 @@
import type { UmbTreeItemModelBase } from './types.js';
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbStore } from '@umbraco-cms/backoffice/store';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';

View File

@@ -0,0 +1,10 @@
export interface UmbTreeRootItemsRequestArgs {
skip: number;
take: number;
}
export interface UmbTreeChildrenOfRequestArgs {
parentUnique: string | null;
skip: number;
take: number;
}

View File

@@ -1,5 +1,5 @@
import type { UmbUniqueTreeItemModel } from '../types.js';
import type { UmbTreeStore } from './tree-store.interface.js';
import type { UmbUniqueTreeItemModel } from './types.js';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';

View File

@@ -1,47 +1,43 @@
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from './reload-tree-item-children/index.js';
import type { UmbTreeItemModelBase } from './types.js';
import type { UmbTreeRepository } from './tree-repository.interface.js';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '../reload-tree-item-children/index.js';
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeRepository } from '../data/tree-repository.interface.js';
import type { UmbTreeContext } from '../tree-context.interface.js';
import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
import {
type ManifestRepository,
type ManifestTree,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbPaginationManager, UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
// TODO: update interface
export interface UmbTreeContext<TreeItemType extends UmbTreeItemModelBase> extends UmbControllerBase {
selection: UmbSelectionManager;
requestChildrenOf: (parentUnique: string | null) => Promise<{
data?: UmbPagedModel<TreeItemType>;
error?: ProblemDetails;
asObservable?: () => Observable<TreeItemType[]>;
}>;
}
export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
extends UmbControllerBase
implements UmbTreeContext<TreeItemType>
export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
extends UmbContextBase<UmbDefaultTreeContext<TreeItemType>>
implements UmbTreeContext
{
#treeRoot = new UmbObjectState<TreeItemType | undefined>(undefined);
treeRoot = this.#treeRoot.asObservable();
public repository?: UmbTreeRepository<TreeItemType>;
public selectableFilter?: (item: TreeItemType) => boolean = () => true;
public filter?: (item: TreeItemType) => boolean = () => true;
public readonly selection = new UmbSelectionManager(this._host);
public readonly pagination = new UmbPaginationManager();
#treeAlias?: string;
#manifest?: ManifestTree;
#repository?: UmbTreeRepository<TreeItemType>;
#actionEventContext?: UmbActionEventContext;
#paging = {
skip: 0,
take: 50,
};
#initResolver?: () => void;
#initialized = false;
@@ -50,9 +46,79 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
});
constructor(host: UmbControllerHostElement) {
super(host);
this.provideContext('umbTreeContext', this);
super(host, UMB_DEFAULT_TREE_CONTEXT);
this.pagination.setPageSize(this.#paging.take);
this.#consumeContexts();
// listen for page changes on the pagination manager
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
this.requestTreeRoot();
}
// TODO: find a generic way to do this
#checkIfInitialized() {
if (this.#repository) {
this.#initialized = true;
this.#initResolver?.();
}
}
/**
* Sets the manifest
* @param {ManifestCollection} manifest
* @memberof UmbCollectionContext
*/
public setManifest(manifest: ManifestTree | undefined) {
if (this.#manifest === manifest) return;
this.#manifest = manifest;
this.#observeRepository(this.#manifest?.meta.repositoryAlias);
}
/**
* Returns the manifest.
* @return {ManifestCollection}
* @memberof UmbCollectionContext
*/
public getManifest() {
return this.#manifest;
}
public getRepository() {
return this.#repository;
}
public async requestTreeRoot() {
await this.#init;
const { data } = await this.#repository!.requestTreeRoot();
if (data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
this.#treeRoot.setValue(data);
}
}
public async requestRootItems() {
await this.#init;
const { data, error, asObservable } = await this.#repository!.requestRootTreeItems({
skip: this.#paging.skip,
take: this.#paging.take,
});
if (data) {
this.pagination.setTotalItems(data.total);
}
return { data, error, asObservable };
}
public async rootItems() {
await this.#init;
return this.#repository!.rootTreeItems();
}
#consumeContexts() {
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
this.#actionEventContext.removeEventListener(
@@ -64,76 +130,15 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
this.#onReloadRequest as EventListener,
);
});
this.requestTreeRoot();
}
// TODO: find a generic way to do this
#checkIfInitialized() {
if (this.repository) {
this.#initialized = true;
this.#initResolver?.();
}
}
#onPageChange = (event: UmbChangeEvent) => {
const target = event.target as UmbPaginationManager;
this.#paging.skip = target.getSkip();
this.requestRootItems();
};
public async setTreeAlias(treeAlias?: string) {
if (this.#treeAlias === treeAlias) return;
this.#treeAlias = treeAlias;
this.#observeTreeManifest();
}
public getTreeAlias() {
return this.#treeAlias;
}
public async requestTreeRoot() {
await this.#init;
const { data } = await this.repository!.requestTreeRoot();
if (data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.#treeRoot.setValue(data);
}
}
public async requestRootItems() {
await this.#init;
return this.repository!.requestRootTreeItems();
}
public async requestChildrenOf(parentUnique: string | null) {
await this.#init;
if (parentUnique === undefined) throw new Error('Parent unique cannot be undefined.');
return this.repository!.requestTreeItemsOf(parentUnique);
}
public async rootItems() {
await this.#init;
return this.repository!.rootTreeItems();
}
public async childrenOf(parentUnique: string | null) {
await this.#init;
return this.repository!.treeItemsOf(parentUnique);
}
#observeTreeManifest() {
if (this.#treeAlias) {
this.observe(
umbExtensionsRegistry.byTypeAndAlias('tree', this.#treeAlias),
async (treeManifest) => {
if (!treeManifest) return;
this.#observeRepository(treeManifest);
},
'_observeTreeManifest',
);
}
}
#observeRepository(treeManifest: ManifestTree) {
const repositoryAlias = treeManifest.meta.repositoryAlias;
#observeRepository(repositoryAlias?: string) {
if (!repositoryAlias) throw new Error('Tree must have a repository alias.');
new UmbExtensionApiInitializer<ManifestRepository<UmbTreeRepository<TreeItemType>>>(
@@ -142,7 +147,7 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
repositoryAlias,
[this._host],
(permitted, ctrl) => {
this.repository = permitted ? ctrl.api : undefined;
this.#repository = permitted ? ctrl.api : undefined;
this.#checkIfInitialized();
},
);
@@ -167,3 +172,7 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
super.destroy();
}
}
export default UmbDefaultTreeContext;
export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken<UmbDefaultTreeContext<any>>('UmbTreeContext');

View File

@@ -0,0 +1,168 @@
import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js';
import type { UmbDefaultTreeContext } from './default-tree.context.js';
import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-default-tree')
export class UmbDefaultTreeElement extends UmbLitElement {
private _selectionConfiguration: UmbTreeSelectionConfiguration = {
multiple: false,
selectable: true,
selection: [],
};
@property({ type: Object, attribute: false })
selectionConfiguration: UmbTreeSelectionConfiguration = this._selectionConfiguration;
@property({ type: Boolean, attribute: false })
hideTreeRoot: boolean = false;
@property({ attribute: false })
selectableFilter: (item: UmbTreeItemModelBase) => boolean = () => true;
@property({ attribute: false })
filter: (item: UmbTreeItemModelBase) => boolean = () => true;
@state()
private _items: UmbTreeItemModelBase[] = [];
@state()
private _treeRoot?: UmbTreeItemModelBase;
@state()
private _currentPage = 1;
@state()
private _totalPages = 1;
#treeContext?: UmbDefaultTreeContext<UmbTreeItemModelBase>;
#init: Promise<unknown>;
constructor() {
super();
this.#init = Promise.all([
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (instance) => {
this.#treeContext = instance;
this.observe(this.#treeContext.pagination.currentPage, (value) => (this._currentPage = value));
this.observe(this.#treeContext.pagination.totalPages, (value) => (this._totalPages = value));
this.#observeTreeRoot();
}).asPromise(),
]);
}
connectedCallback(): void {
super.connectedCallback();
this.#init;
}
protected async updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): Promise<void> {
super.updated(_changedProperties);
await this.#init;
if (_changedProperties.has('selectionConfiguration')) {
this._selectionConfiguration = this.selectionConfiguration;
this.#treeContext!.selection.setMultiple(this._selectionConfiguration.multiple ?? false);
this.#treeContext!.selection.setSelectable(this._selectionConfiguration.selectable ?? true);
this.#treeContext!.selection.setSelection(this._selectionConfiguration.selection ?? []);
}
if (_changedProperties.has('hideTreeRoot')) {
if (this.hideTreeRoot === true) {
this.#observeRootItems();
}
}
if (_changedProperties.has('selectableFilter')) {
this.#treeContext!.selectableFilter = this.selectableFilter;
}
if (_changedProperties.has('filter')) {
this.#treeContext!.filter = this.filter;
}
}
#observeTreeRoot() {
if (!this.#treeContext) return;
this.observe(
this.#treeContext.treeRoot,
(treeRoot) => {
this._treeRoot = treeRoot;
},
'umbTreeRootObserver',
);
}
async #observeRootItems() {
await this.#init;
if (!this.#treeContext?.requestRootItems) throw new Error('Tree does not support root items');
const { asObservable } = await this.#treeContext.requestRootItems();
if (asObservable) {
this.observe(
asObservable(),
(rootItems) => {
const oldValue = this._items;
this._items = rootItems;
this.requestUpdate('_items', oldValue);
},
'umbRootItemsObserver',
);
}
}
getSelection() {
return this.#treeContext?.selection.getSelection();
}
render() {
return html` ${this.#renderTreeRoot()} ${this.#renderRootItems()}`;
}
#renderTreeRoot() {
if (this.hideTreeRoot || this._treeRoot === undefined) return nothing;
return html`
<umb-tree-item .entityType=${this._treeRoot.entityType} .props=${{ item: this._treeRoot }}></umb-tree-item>
`;
}
#renderRootItems() {
if (this._items?.length === 0) return nothing;
return html`
${repeat(
this._items,
(item, index) => item.name + '___' + index,
(item) => html`<umb-tree-item .entityType=${item.entityType} .props=${{ item }}></umb-tree-item>`,
)}
${this.#renderPaging()}
`;
}
#onLoadMoreClick = (event: any) => {
event.stopPropagation();
const next = (this._currentPage = this._currentPage + 1);
this.#treeContext?.pagination.setCurrentPageNumber(next);
};
#renderPaging() {
if (this._totalPages <= 1 || this._currentPage === this._totalPages) {
return nothing;
}
return html` <uui-button @click=${this.#onLoadMoreClick} label="Load more"></uui-button> `;
}
}
export default UmbDefaultTreeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-default-tree': UmbDefaultTreeElement;
}
}

View File

@@ -0,0 +1,2 @@
export { UmbDefaultTreeElement as UmbTreeDefaultElement } from './default-tree.element.js';
export { UmbDefaultTreeContext as UmbTreeDefaultContext } from './default-tree.context.js';

View File

@@ -0,0 +1,15 @@
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
const defaultTreeKind: UmbBackofficeManifestKind = {
type: 'kind',
alias: 'Umb.Kind.Tree.Default',
matchKind: 'default',
matchType: 'tree',
manifest: {
type: 'tree',
api: () => import('./default-tree.context.js'),
element: () => import('./default-tree.element.js'),
},
};
export const manifests = [defaultTreeKind];

View File

@@ -1,27 +1,14 @@
export * from './components/index.js';
export * from './tree-item-default/index.js';
export * from './tree-item-base/index.js';
export * from './tree-item/index.js';
export * from './default/index.js';
export * from './data/index.js';
export * from './tree-menu-item-default/index.js';
export * from './tree.context.js';
export * from './tree.element.js';
export * from './types.js';
export * from './tree-repository.interface.js';
export * from './tree-store.interface.js';
// Unique
export * from './unique-tree-store.js';
export * from './unique-tree-item/index.js';
// Data Source
export * from './data-source/index.js';
// Folder
export * from './folder/index.js';
export * from './tree.element.js';
//
export {
UmbReloadTreeItemChildrenEntityAction,
UmbReloadTreeItemChildrenRequestEntityActionEvent,
} from './reload-tree-item-children/index.js';
export { UmbTreeRepositoryBase } from './tree-repository-base.js';
export * from './types.js';

View File

@@ -1,3 +1,11 @@
import { manifests as folderManifests } from './folder/manifests.js';
import { manifests as defaultTreeItemManifests } from './tree-item/tree-item-default/manifests.js';
import { manifests as defaultTreeManifests } from './default/manifests.js';
import { manifests as treePickerManifests } from './tree-picker/manifests.js';
export const manifests = [...folderManifests];
export const manifests = [
...defaultTreeManifests,
...folderManifests,
...defaultTreeItemManifests,
...treePickerManifests,
];

View File

@@ -0,0 +1,7 @@
import type { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
// TODO: update interface
export interface UmbTreeContext extends UmbContextBase<UmbTreeContext> {
selection: UmbSelectionManager;
}

View File

@@ -1,2 +0,0 @@
export * from './tree-item-base.context.js';
export * from './tree-item-base.element.js';

View File

@@ -1,20 +0,0 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import './tree-item-base.element.js';
import type { UmbTreeItemBaseElement } from './tree-item-base.element.js';
// TODO: provide tree item context to make this element render properly
const meta: Meta<UmbTreeItemBaseElement> = {
title: 'Components/Tree/Tree Item Default',
component: 'umb-tree-item-default',
};
export default meta;
type Story = StoryObj<UmbTreeItemBaseElement>;
export const Overview: Story = {
args: {},
};
export const WithChildren: Story = {
args: {},
};

View File

@@ -1,2 +0,0 @@
export * from './tree-item.context.interface.js';
export * from './tree-item.element.js';

View File

@@ -1,36 +0,0 @@
import type { UmbTreeItemModelBase } from '../types.js';
import { css, html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-tree-item-default')
export class UmbTreeItemDefaultElement extends UmbLitElement {
@property({ type: Object, attribute: false })
item?: UmbTreeItemModelBase;
render() {
if (!this.item) return nothing;
return html`<umb-extension-slot
type="treeItem"
.filter=${(manifests: ManifestTreeItem) => manifests.meta.entityTypes.includes(this.item!.entityType)}
.props=${{
item: this.item,
}}></umb-extension-slot>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-tree-item-default': UmbTreeItemDefaultElement;
}
}

View File

@@ -0,0 +1,4 @@
export * from './tree-item-context.interface.js';
export * from './tree-item.element.js';
export * from './tree-item-default/index.js';
export * from './tree-item-base/index.js';

View File

@@ -0,0 +1,2 @@
export * from './tree-item-context-base.js';
export * from './tree-item-element-base.js';

View File

@@ -1,28 +1,34 @@
import type { UmbTreeItemContext } from '../tree-item-default/tree-item.context.interface.js';
import type { UmbTreeContextBase } from '../tree.context.js';
import type { UmbTreeItemModelBase } from '../types.js';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '../reload-tree-item-children/index.js';
import type { UmbTreeItemContext } from '../tree-item-context.interface.js';
import { UMB_DEFAULT_TREE_CONTEXT, type UmbDefaultTreeContext } from '../../default/default-tree.context.js';
import type { UmbTreeItemModelBase } from '../../types.js';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '../../reload-tree-item-children/index.js';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UMB_SECTION_CONTEXT, UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/backoffice/section';
import type { UmbSectionContext, UmbSectionSidebarContext } from '@umbraco-cms/backoffice/section';
import type { ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbBooleanState, UmbDeepState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UMB_ACTION_EVENT_CONTEXT, type UmbActionEventContext } from '@umbraco-cms/backoffice/action';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type UmbTreeItemUniqueFunction<TreeItemType extends UmbTreeItemModelBase> = (
x: TreeItemType,
) => string | null | undefined;
export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
extends UmbControllerBase
export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
extends UmbContextBase<UmbTreeItemContext<TreeItemType>>
implements UmbTreeItemContext<TreeItemType>
{
public unique?: string | null;
public entityType?: string;
public readonly pagination = new UmbPaginationManager();
#manifest?: ManifestTreeItem;
#treeItem = new UmbDeepState<TreeItemType | undefined>(undefined);
treeItem = this.#treeItem.asObservable();
@@ -52,17 +58,46 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
#path = new UmbStringState('');
path = this.#path.asObservable();
treeContext?: UmbTreeContextBase<TreeItemType>;
treeContext?: UmbDefaultTreeContext<TreeItemType>;
#sectionContext?: UmbSectionContext;
#sectionSidebarContext?: UmbSectionSidebarContext;
#actionEventContext?: UmbActionEventContext;
#getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>;
// TODO: get this from the tree context
#paging = {
skip: 0,
take: 50,
};
constructor(host: UmbControllerHost, getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>) {
super(host);
super(host, UMB_TREE_ITEM_CONTEXT);
this.pagination.setPageSize(this.#paging.take);
this.#getUniqueFunction = getUniqueFunction;
this.#consumeContexts();
this.provideContext(UMB_TREE_ITEM_CONTEXT, this);
// listen for page changes on the pagination manager
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
}
/**
* Sets the manifest
* @param {ManifestCollection} manifest
* @memberof UmbCollectionContext
*/
// TODO: Revisit if this instead should be a getter/setter property because it might be set by extension initializer
public setManifest(manifest: ManifestTreeItem | undefined) {
if (this.#manifest === manifest) return;
this.#manifest = manifest;
}
/**
* Returns the manifest.
* @return {ManifestCollection}
* @memberof UmbCollectionContext
*/
public getManifest() {
return this.#manifest;
}
public setTreeItem(treeItem: TreeItemType | undefined) {
@@ -91,12 +126,23 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
public async requestChildren() {
if (this.unique === undefined) throw new Error('Could not request children, unique key is missing');
// TODO: wait for tree context to be ready
const repository = this.treeContext?.getRepository();
if (!repository) throw new Error('Could not request children, repository is missing');
this.#isLoading.setValue(true);
const response = await this.treeContext!.requestChildrenOf(this.unique);
const { data, error, asObservable } = await repository.requestTreeItemsOf({
parentUnique: this.unique,
skip: this.#paging.skip,
take: this.#paging.take,
});
if (data) {
this.pagination.setTotalItems(data.total);
}
this.#isLoading.setValue(false);
return response;
return { data, error, asObservable };
}
public toggleContextMenu() {
@@ -127,7 +173,7 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
this.#sectionSidebarContext = instance;
});
this.consumeContext('umbTreeContext', (treeContext: UmbTreeContextBase<TreeItemType>) => {
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (treeContext: UmbDefaultTreeContext<TreeItemType>) => {
this.treeContext = treeContext;
this.#observeIsSelectable();
this.#observeIsSelected();
@@ -209,10 +255,16 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
async #observeHasChildren() {
if (!this.treeContext || !this.unique) return;
const observable = await this.treeContext.childrenOf(this.unique);
const repository = this.treeContext.getRepository();
if (!repository) return;
// TODO: use createObservablePart, to prevent unnesecary changes.
const hasChildrenObservable = (await repository.treeItemsOf(this.unique)).pipe(
map((children) => children.length > 0),
);
// observe if any children will be added runtime to a tree item. Nested items/folders etc.
this.observe(observable.pipe(map((children) => children.length > 0)), (hasChildren) => {
this.observe(hasChildrenObservable, (hasChildren) => {
// we need to skip the first value, because it will also return false until a child is in the store
// we therefor rely on the value from the tree item itself
if (this.#hasChildrenInitValueFlag === true) {
@@ -230,6 +282,12 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
this.requestChildren();
};
#onPageChange = (event: UmbChangeEvent) => {
const target = event.target as UmbPaginationManager;
this.#paging.skip = target.getSkip();
this.requestChildren();
};
// TODO: use router context
constructPath(pathname: string, entityType: string, unique: string | null) {
return `section/${pathname}/workspace/${entityType}/edit/${unique}`;

View File

@@ -1,16 +1,23 @@
import type { UmbTreeItemContext } from '../tree-item-default/index.js';
import type { UmbTreeItemModelBase } from '../types.js';
import { UMB_TREE_ITEM_CONTEXT } from './tree-item-base.context.js';
import { html, nothing, customElement, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import type { UmbTreeItemContext } from '../index.js';
import type { UmbTreeItemModelBase } from '../../types.js';
import { UMB_TREE_ITEM_CONTEXT } from './tree-item-context-base.js';
import { html, nothing, state, ifDefined, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-tree-item-base')
export class UmbTreeItemBaseElement extends UmbLitElement {
@state()
private _item?: UmbTreeItemModelBase;
// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name
export abstract class UmbTreeItemElementBase<TreeItemModelType extends UmbTreeItemModelBase> extends UmbLitElement {
_item?: TreeItemModelType;
@property({ type: Object, attribute: false })
get item(): TreeItemModelType | undefined {
return this._item;
}
set item(newVal: TreeItemModelType) {
this._item = newVal;
this.#initTreeItem();
}
@state()
private _childItems?: UmbTreeItemModelBase[];
private _childItems?: TreeItemModelType[];
@state()
private _href?: string;
@@ -33,7 +40,13 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
@state()
private _iconSlotHasChildren = false;
#treeItemContext?: UmbTreeItemContext<UmbTreeItemModelBase>;
@state()
private _totalPages = 1;
@state()
private _currentPage = 1;
#treeItemContext?: UmbTreeItemContext<TreeItemModelType>;
constructor() {
super();
@@ -41,6 +54,9 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
this.consumeContext(UMB_TREE_ITEM_CONTEXT, (instance) => {
this.#treeItemContext = instance;
if (!this.#treeItemContext) return;
this.#initTreeItem();
// TODO: investigate if we can make an observe decorator
this.observe(this.#treeItemContext.treeItem, (value) => (this._item = value));
this.observe(this.#treeItemContext.hasChildren, (value) => (this._hasChildren = value));
@@ -49,9 +65,17 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
this.observe(this.#treeItemContext.isSelectable, (value) => (this._isSelectable = value));
this.observe(this.#treeItemContext.isSelected, (value) => (this._isSelected = value));
this.observe(this.#treeItemContext.path, (value) => (this._href = value));
this.observe(this.#treeItemContext.pagination.currentPage, (value) => (this._currentPage = value));
this.observe(this.#treeItemContext.pagination.totalPages, (value) => (this._totalPages = value));
});
}
#initTreeItem() {
if (!this.#treeItemContext) return;
if (!this._item) return;
this.#treeItemContext.setTreeItem(this._item);
}
private _handleSelectedItem(event: Event) {
event.stopPropagation();
this.#treeItemContext?.select();
@@ -81,9 +105,11 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
});
}
private _openActions() {
this.#treeItemContext?.toggleContextMenu();
}
#onLoadMoreClick = (event: any) => {
event.stopPropagation();
const next = (this._currentPage = this._currentPage + 1);
this.#treeItemContext?.pagination.setCurrentPageNumber(next);
};
// Note: Currently we want to prevent opening when the item is in a selectable context, but this might change in the future.
// If we like to be able to open items in selectable context, then we might want to make it as a menu item action, so you have to click ... and chose an action called 'Edit'
@@ -100,8 +126,9 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
.hasChildren=${this._hasChildren}
label="${ifDefined(this._item?.name)}"
href="${ifDefined(this._isSelectableContext ? undefined : this._href)}">
${this.#renderIconContainer()} ${this.#renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()}
${this.renderIconContainer()} ${this.renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()}
<slot></slot>
${this.#renderPaging()}
</uui-menu-item>
`;
}
@@ -110,7 +137,7 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
return (e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0;
};
#renderIconContainer() {
renderIconContainer() {
return html`
<slot
name="icon"
@@ -137,7 +164,7 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
return html`<uui-icon slot="icon" name="icon-circle-dotted"></uui-icon>`;
}
#renderLabel() {
renderLabel() {
return html`<slot name="label" slot="label"></slot>`;
}
@@ -157,17 +184,18 @@ export class UmbTreeItemBaseElement extends UmbLitElement {
${this._childItems
? repeat(
this._childItems,
// TODO: get unique here instead of name. we might be able to get it from the context
(item) => item.name,
(item) => html`<umb-tree-item-default .item=${item}></umb-tree-item-default>`,
(item, index) => item.name + '___' + index,
(item) => html`<umb-tree-item .entityType=${item.entityType} .props=${{ item }}></umb-tree-item>`,
)
: ''}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-tree-item-base': UmbTreeItemBaseElement;
#renderPaging() {
if (this._totalPages <= 1 || this._currentPage === this._totalPages) {
return nothing;
}
return html` <uui-button @click=${this.#onLoadMoreClick} label="Load more"></uui-button> `;
}
}

View File

@@ -1,9 +1,11 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbPaginationManager } from '../../utils/pagination-manager/pagination.manager.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbTreeItemContext<TreeItemType extends UmbTreeItemModelBase> {
export interface UmbTreeItemContext<TreeItemType extends UmbTreeItemModelBase> extends UmbApi {
unique?: string | null;
entityType?: string;
treeItem: Observable<TreeItemType | undefined>;
@@ -15,7 +17,7 @@ export interface UmbTreeItemContext<TreeItemType extends UmbTreeItemModelBase> {
isActive: Observable<boolean>;
hasActions: Observable<boolean>;
path: Observable<string>;
pagination: UmbPaginationManager;
setTreeItem(treeItem: TreeItemType | undefined): void;
requestChildren(): Promise<{
data?: UmbPagedModel<TreeItemType> | undefined;

View File

@@ -0,0 +1,2 @@
export * from './tree-item-default.context.js';
export * from './tree-item-default.element.js';

View File

@@ -0,0 +1,15 @@
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
const kind: UmbBackofficeManifestKind = {
type: 'kind',
alias: 'Umb.Kind.TreeItem.Default',
matchKind: 'default',
matchType: 'treeItem',
manifest: {
type: 'treeItem',
api: () => import('./tree-item-default.context.js'),
element: () => import('./tree-item-default.element.js'),
},
};
export const manifests = [kind];

View File

@@ -1,11 +1,13 @@
import { UmbTreeItemContextBase } from '../tree-item-base/tree-item-base.context.js';
import type { UmbUniqueTreeItemModel } from '../types.js';
import { UmbTreeItemContextBase } from '../tree-item-base/index.js';
import type { UmbUniqueTreeItemModel } from '../../types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbUniqueTreeItemContext<
export class UmbDefaultTreeItemContext<
TreeItemModelType extends UmbUniqueTreeItemModel,
> extends UmbTreeItemContextBase<TreeItemModelType> {
constructor(host: UmbControllerHost) {
super(host, (x: UmbUniqueTreeItemModel) => x.unique);
}
}
export default UmbDefaultTreeItemContext;

View File

@@ -0,0 +1,14 @@
import { UmbTreeItemElementBase } from '../tree-item-base/index.js';
import type { UmbUniqueTreeItemModel } from '../../types.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-default-tree-item')
export class UmbDefaultTreeItemElement extends UmbTreeItemElementBase<UmbUniqueTreeItemModel> {}
export default UmbDefaultTreeItemElement;
declare global {
interface HTMLElementTagNameMap {
'umb-default-tree-item': UmbDefaultTreeItemElement;
}
}

View File

@@ -0,0 +1,51 @@
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionInitializerElementBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-tree-item')
export class UmbTreeItemElement extends UmbExtensionInitializerElementBase<ManifestTreeItem> {
_entityType?: string;
@property({ type: String, reflect: true })
get entityType() {
return this._entityType;
}
set entityType(newVal) {
this._entityType = newVal;
this.#observeManifest();
}
#observeManifest() {
if (!this._entityType) return;
const filterByEntityType = (manifest: ManifestTreeItem) => {
if (!this._entityType) return false;
return manifest.meta.entityTypes.includes(this._entityType);
};
this.observe(
umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType),
(manifests) => {
if (!manifests) return;
// TODO: what should we do if there are multiple tree items for an entity type?
const manifest = manifests[0];
this.createApi(manifest);
this.createElement(manifest);
},
'umbObserveTreeManifest',
);
}
getExtensionType() {
return 'treeItem';
}
getDefaultElementName() {
return 'umb-default-tree-item';
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-tree-item': UmbTreeItemElement;
}
}

View File

@@ -30,7 +30,13 @@ export class UmbMenuItemTreeDefaultElement extends UmbLitElement implements UmbM
? html`
<umb-tree
alias=${this.manifest?.meta.treeAlias}
?hide-tree-root=${this.manifest.meta.hideTreeRoot === true}></umb-tree>
.props=${{
hideTreeRoot: this.manifest?.meta.hideTreeRoot === true,
selectionConfiguration: {
selectable: false,
multiple: false,
},
}}></umb-tree>
`
: nothing;
}

View File

@@ -0,0 +1,8 @@
export const manifests = [
{
type: 'modal',
alias: 'Umb.Modal.TreePicker',
name: 'Tree Picker Modal',
js: () => import('./tree-picker-modal.element.js'),
},
];

View File

@@ -1,8 +1,9 @@
import type { UmbTreeSelectionConfiguration } from '../types.js';
import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UmbTreePickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbDeselectedEvent, UmbSelectedEvent, UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbTreeElement, UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeElement, UmbTreeItemModelBase } from '@umbraco-cms/backoffice/tree';
@customElement('umb-tree-picker-modal')
export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase> extends UmbModalBaseElement<
@@ -51,14 +52,16 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
<umb-body-layout headline="Select">
<uui-box>
<umb-tree
?hide-tree-root=${this.data?.hideTreeRoot}
alias=${ifDefined(this.data?.treeAlias)}
.props=${{
hideTreeRoot: this.data?.hideTreeRoot,
selectionConfiguration: this._selectionConfiguration,
filter: this.data?.filter,
selectableFilter: this.data?.pickableFilter,
}}
@selection-change=${this.#onSelectionChange}
@selected=${this.#onSelected}
@deselected=${this.#onDeselected}
.selectionConfiguration=${this._selectionConfiguration}
.filter=${this.data?.filter}
.selectableFilter=${this.data?.pickableFilter}></umb-tree>
@deselected=${this.#onDeselected}></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>

View File

@@ -1,139 +1,22 @@
import { UmbTreeContextBase } from './tree.context.js';
import type { UmbTreeItemModelBase } from './types.js';
import { html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import './tree-item-default/tree-item.element.js';
import './tree-item-base/tree-item-base.element.js';
export type UmbTreeSelectionConfiguration = {
multiple?: boolean;
selectable?: boolean;
selection?: Array<string | null>;
};
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import type { ManifestTree } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionInitializerElementBase } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-tree')
export class UmbTreeElement extends UmbLitElement {
@property({ type: String, reflect: true })
set alias(newVal) {
this.#treeContext.setTreeAlias(newVal);
}
get alias() {
return this.#treeContext.getTreeAlias();
export class UmbTreeElement extends UmbExtensionInitializerElementBase<ManifestTree> {
getExtensionType() {
return 'tree';
}
private _selectionConfiguration: UmbTreeSelectionConfiguration = {
multiple: false,
selectable: true,
selection: [],
};
@property({ type: Object })
set selectionConfiguration(config: UmbTreeSelectionConfiguration) {
this._selectionConfiguration = config;
this.#treeContext.selection.setMultiple(config.multiple ?? false);
this.#treeContext.selection.setSelectable(config.selectable ?? true);
this.#treeContext.selection.setSelection(config.selection ?? []);
}
get selectionConfiguration(): UmbTreeSelectionConfiguration {
return this._selectionConfiguration;
}
// TODO: what is the best name for this functionality?
private _hideTreeRoot = false;
@property({ type: Boolean, attribute: 'hide-tree-root' })
set hideTreeRoot(newVal: boolean) {
const oldVal = this._hideTreeRoot;
this._hideTreeRoot = newVal;
if (newVal === true) {
this.#observeRootItems();
}
this.requestUpdate('hideTreeRoot', oldVal);
}
get hideTreeRoot() {
return this._hideTreeRoot;
}
@property()
set selectableFilter(newVal) {
this.#treeContext.selectableFilter = newVal;
}
get selectableFilter() {
return this.#treeContext.selectableFilter;
}
@property()
set filter(newVal) {
this.#treeContext.filter = newVal;
}
get filter() {
return this.#treeContext.filter;
}
@state()
private _items: UmbTreeItemModelBase[] = [];
@state()
private _treeRoot?: UmbTreeItemModelBase;
#treeContext = new UmbTreeContextBase<UmbTreeItemModelBase>(this);
#rootItemsObserver?: UmbObserverController<Array<UmbTreeItemModelBase>>;
constructor() {
super();
this.#observeTreeRoot();
}
#observeTreeRoot() {
this.observe(
this.#treeContext.treeRoot,
(treeRoot) => {
this._treeRoot = treeRoot;
},
'umbTreeRootObserver',
);
}
async #observeRootItems() {
if (!this.#treeContext?.requestRootItems) throw new Error('Tree does not support root items');
this.#rootItemsObserver?.destroy();
const { asObservable } = await this.#treeContext.requestRootItems();
if (asObservable) {
this.#rootItemsObserver = this.observe(asObservable(), (rootItems) => {
const oldValue = this._items;
this._items = rootItems;
this.requestUpdate('_items', oldValue);
});
}
getDefaultElementName() {
return 'umb-default-tree';
}
getSelection() {
return this.#treeContext.selection.getSelection();
}
render() {
return html` ${this.#renderTreeRoot()} ${this.#renderRootItems()}`;
}
#renderTreeRoot() {
if (this.hideTreeRoot || this._treeRoot === undefined) return nothing;
return html` <umb-tree-item-default .item=${this._treeRoot}></umb-tree-item-default> `;
}
#renderRootItems() {
if (this._items?.length === 0) return nothing;
return html`
${repeat(
this._items,
// TODO: use unique here:
(item, index) => item.name + '___' + index,
(item) => html`<umb-tree-item-default .item=${item}></umb-tree-item-default>`,
)}
`;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: make base interface for a tree element
return this._element?.getSelection?.() ?? [];
}
}

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import './tree.element.js';
import './default/default-tree.element.js';
import type { UmbTreeElement } from './tree.element.js';
const meta: Meta<UmbTreeElement> = {

View File

@@ -14,3 +14,9 @@ export interface UmbUniqueTreeItemModel extends UmbTreeItemModelBase {
export interface UmbUniqueTreeRootModel extends UmbTreeItemModelBase {
unique: null;
}
export type UmbTreeSelectionConfiguration = {
multiple?: boolean;
selectable?: boolean;
selection?: Array<string | null>;
};

Some files were not shown because too many files have changed in this diff Show More