Merge branch 'main' into improvement/model-remapping

This commit is contained in:
Mads Rasmussen
2024-02-06 12:56:14 +01:00
33 changed files with 1122 additions and 223 deletions

View File

@@ -62,6 +62,7 @@
"./package": "./dist-cms/packages/packages/package/index.js",
"./data-type": "./dist-cms/packages/core/data-type/index.js",
"./language": "./dist-cms/packages/language/index.js",
"./dynamic-root": "./dist-cms/packages/dynamic-root/index.js",
"./logviewer": "./dist-cms/packages/settings/logviewer/index.js",
"./relation-type": "./dist-cms/packages/relations/relation-types/index.js",
"./relation": "./dist-cms/packages/relations/relations/index.js",

View File

@@ -32,6 +32,11 @@ const CORE_PACKAGES = [
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/user/umbraco-package.js'),
import('../../packages/models-builder/umbraco-package.js'),
import('../../packages/tags/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
import('../../packages/health-check/umbraco-package.js'),
import('../../packages/static-file/umbraco-package.js'),
import('../../packages/dynamic-root/umbraco-package.js'),
];
@customElement('umb-backoffice')

View File

@@ -1156,6 +1156,38 @@ export default {
pickedTrashedItem: 'Du har valgt et dokument som er slettet eller lagt i papirkurven',
pickedTrashedItems: 'Du har valgt dokumenter som er slettede eller lagt i papirkurven',
},
dynamicRoot: {
configurationTitle: 'Dynamisk udgangspunkts forespørgsel',
pickDynamicRootOriginTitle: 'Vælg begyndelsen',
pickDynamicRootOriginDesc: 'Beskriv begyndelsen for dynamisk udgangspunkts forespørgselen',
originRootTitle: 'Roden',
originRootDesc: 'Rod noden for denne kilde',
originParentTitle: 'Overliggende',
originParentDesc: 'Den overliggende node af kilden i denne redigerings session',
originCurrentTitle: 'Nuværende',
originCurrentDesc: 'Kilde noden for denne redigerings session',
originSiteTitle: 'Siden',
originSiteDesc: 'Nærmeste node med et domæne',
originByKeyTitle: 'Specifik Node',
originByKeyDesc: 'Vælg en specifik Node',
pickDynamicRootQueryStepTitle: 'Tilføj skridt til forespørgsel',
pickDynamicRootQueryStepDesc: 'Specificer næste skridt i din dynamisk udgangspunkts forespørgsel',
queryStepNearestAncestorOrSelfTitle: 'Nærmeste forældre eller selv',
queryStepNearestAncestorOrSelfDesc: 'Forespørg the nærmeste forældre eller selv der passer på en af de givne typer',
queryStepFurthestAncestorOrSelfTitle: 'Fjerneste forældre eller selv',
queryStepFurthestAncestorOrSelfDesc: 'Forespørg fjerneste forældre eller selv der passer på en af de givne typer',
queryStepNearestDescendantOrSelfTitle: 'Nærmeste barn eller selv',
queryStepNearestDescendantOrSelfDesc: 'Forespørg nærmeste barn eller selv der passer på en af de givne typer',
queryStepFurthestDescendantOrSelfTitle: 'Fjerneste barn eller selv',
queryStepFurthestDescendantOrSelfDesc: 'Forespørg fjerneste barn eller selv der passer på en af de givne typer',
queryStepCustomTitle: 'Brugerdefineret',
queryStepCustomDesc: 'Forespørg med et skræddersyet forespørgsels skridt',
addQueryStep: 'Tilføj skridt',
queryStepTypes: 'der passer med typerne: ',
noValidStartNodeTitle: 'Intet passende indhold',
noValidStartNodeDesc:
'Konfigurationen af dette felt passer ikke med noget indhold. Opret det manglende indhold eller kontakt din adminnistrator for at tilpasse Dynamisk Udgangspunkts Forespørgselen for dette felt.',
},
mediaPicker: {
deletedItem: 'Slettet medie',
pickedTrashedItem: 'Du har valgt et medie som er slettet eller lagt i papirkurven',

View File

@@ -1148,11 +1148,46 @@ export default {
},
contentPicker: {
allowedItemTypes: 'You can only select items of type(s): %0%',
defineDynamicRoot: 'Specify a Dynamic Root',
defineDynamicRoot: 'Specify root node',
defineRootNode: 'Pick root node',
pickedTrashedItem: 'You have picked a content item currently deleted or in the recycle bin',
pickedTrashedItems: 'You have picked content items currently deleted or in the recycle bin',
},
dynamicRoot: {
configurationTitle: 'Dynamic Root Query',
pickDynamicRootOriginTitle: 'Pick origin',
pickDynamicRootOriginDesc: 'Define the origin for your Dynamic Root Query',
originRootTitle: 'Root',
originRootDesc: 'Root node of this editing session',
originParentTitle: 'Parent',
originParentDesc: 'The parent node of the source in this editing session',
originCurrentTitle: 'Current',
originCurrentDesc: 'The content node that is source for this editing session',
originSiteTitle: 'Site',
originSiteDesc: 'Find nearest node with a hostname',
originByKeyTitle: 'Specific Node',
originByKeyDesc: 'Pick a specific Node as the origin for this query',
pickDynamicRootQueryStepTitle: 'Append step to query',
pickDynamicRootQueryStepDesc: 'Define the next step of your Dynamic Root Query',
queryStepNearestAncestorOrSelfTitle: 'Nearest Ancestor Or Self',
queryStepNearestAncestorOrSelfDesc: 'Query the nearest ancestor or self that fits with one of the configured types',
queryStepFurthestAncestorOrSelfTitle: 'Furthest Ancestor Or Self',
queryStepFurthestAncestorOrSelfDesc:
'Query the Furthest ancestor or self that fits with one of the configured types',
queryStepNearestDescendantOrSelfTitle: 'Nearest Descendant Or Self',
queryStepNearestDescendantOrSelfDesc:
'Query the nearest descendant or self that fits with one of the configured types',
queryStepFurthestDescendantOrSelfTitle: 'Furthest Descendant Or Self',
queryStepFurthestDescendantOrSelfDesc:
'Query the Furthest descendant or self that fits with one of the configured types',
queryStepCustomTitle: 'Custom',
queryStepCustomDesc: 'Query the using a custom Query Step',
addQueryStep: 'Add query step',
queryStepTypes: 'That matches types: ',
noValidStartNodeTitle: 'No matching content',
noValidStartNodeDesc:
'The configuration of this property does not match any content. Create the missing content or contact your administrator to adjust the Dynamic Root settings for this property.',
},
mediaPicker: {
deletedItem: 'Deleted item',
pickedTrashedItem: 'You have picked a media item currently deleted or in the recycle bin',

View File

@@ -5,6 +5,7 @@ import { handlers as dataTypeHandlers } from './handlers/data-type/index.js';
import { handlers as dictionaryHandlers } from './handlers/dictionary/index.js';
import { handlers as documentHandlers } from './handlers/document/index.js';
import { handlers as documentTypeHandlers } from './handlers/document-type/index.js';
import { handlers as dynamicRootHandlers } from './handlers/dynamic-root.handlers.js';
import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js';
import { handlers as healthCheckHandlers } from './handlers/health-check.handlers.js';
import { handlers as installHandlers } from './handlers/install.handlers.js';
@@ -45,6 +46,7 @@ const handlers = [
...dictionaryHandlers,
...documentHandlers,
...documentTypeHandlers,
...dynamicRootHandlers,
...examineManagementHandlers,
...healthCheckHandlers,
...installHandlers,

View File

@@ -225,6 +225,15 @@ export const data: Array<UmbMockDataTypeModel> = [
value: {
type: 'content',
id: null,
dynamicRoot: {
originAlias: 'Root',
querySteps: [
{
alias: 'FurthestAncestorOrSelf',
anyOfDocTypeKeys: ['all-property-editors-document-type-id'],
},
],
},
},
},
{

View File

@@ -0,0 +1,15 @@
import { umbDocumentMockDb } from '../data/document/document.db.js';
import type { DynamicRootRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
const { rest } = window.MockServiceWorker;
export const handlers = [
rest.post<DynamicRootRequestModel>(umbracoPath('/dynamic-root/query'), async (req, res, ctx) => {
const response = umbDocumentMockDb.tree
.getRoot()
.items.map((item) => item.id)
.slice(0, 1);
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -1,108 +1,154 @@
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import {
UMB_BLOCK_CATALOGUE_MODAL,
type UmbBlockLayoutBaseModel,
type UmbBlockTypeBaseModel,
type UmbBlockTypeGroup,
} from '@umbraco-cms/backoffice/block';
import { type UmbModalRouteBuilder, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
/**
* @element umb-property-editor-ui-block-grid
*/
@customElement('umb-property-editor-ui-block-grid')
export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implements UmbPropertyEditorUiElement {
#catalogueModal: UmbModalRouteRegistrationController<typeof UMB_BLOCK_CATALOGUE_MODAL.DATA, undefined>;
@property()
value = '';
@state()
private _limitMin?: number;
@state()
private _limitMax?: number;
@state()
private _blocks?: Array<UmbBlockTypeBaseModel>;
@state()
private _blockGroups?: Array<UmbBlockTypeGroup>;
@state()
private _layouts: Array<UmbBlockLayoutBaseModel> = [];
@state()
private _catalogueRouteBuilder?: UmbModalRouteBuilder;
@state()
private _directRoute?: string;
@state()
private _createButtonLabel = this.localize.term('blockEditor_addBlock');
@property({ attribute: false })
public config?: UmbPropertyEditorConfigCollection;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@state()
private _routes: UmbRoute[] = [];
const validationLimit = config.getValueByAlias<NumberRangeValueType>('validationLimit');
@state()
private _routerPath: string | undefined;
this._limitMin = validationLimit?.min;
this._limitMax = validationLimit?.max;
@state()
private _activePath: string | undefined;
this._blocks = config.getValueByAlias<Array<UmbBlockTypeBaseModel>>('blocks') ?? [];
this._blockGroups = config.getValueByAlias<Array<UmbBlockTypeGroup>>('blockGroups') ?? [];
@state()
private _variantId?: UmbVariantId;
const customCreateButtonLabel = config.getValueByAlias<string>('createLabel');
if (customCreateButtonLabel) {
this._createButtonLabel = customCreateButtonLabel;
} else if (this._blocks.length === 1) {
this._createButtonLabel = this.localize.term('blockEditor_addThis', [this._blocks[0].label]);
}
//const useInlineEditingAsDefault = config.getValueByAlias<boolean>('useInlineEditingAsDefault');
//this.#context.setInlineEditingMode(useInlineEditingAsDefault);
//config.useSingleBlockMode
//config.useLiveEditing
//config.useInlineEditingAsDefault
this.style.maxWidth = config.getValueByAlias<string>('maxPropertyWidth') ?? '';
//this.#context.setEditorConfiguration(config);
}
constructor() {
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this.observe(context?.variantId, (propertyVariantId) => {
this._variantId = propertyVariantId;
this.setupRoutes();
this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => {
this.observe(
propertyContext?.alias,
(alias) => {
this.#catalogueModal.setUniquePathValue('propertyAlias', alias);
},
'observePropertyAlias',
);
});
});
}
setupRoutes() {
this._routes = [];
if (this._variantId !== undefined) {
this._routes = [
{
path: 'modal-1',
component: () => {
return import('./property-editor-ui-block-grid-inner-test.element.js');
this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
.addUniquePaths(['propertyAlias'])
.addAdditionalPath(':view/:index')
.onSetup((routingInfo) => {
const index = routingInfo.index ? parseInt(routingInfo.index) : -1;
return {
data: {
blocks: this._blocks ?? [],
blockGroups: this._blockGroups ?? [],
openClipboard: routingInfo.view === 'clipboard',
blockOriginData: { index: index },
},
setup: (component) => {
if (component instanceof HTMLElement) {
(component as any).name = 'block-grid-1';
}
},
},
{
path: 'modal-2',
component: () => {
return import('./property-editor-ui-block-grid-inner-test.element.js');
},
setup: (component) => {
if (component instanceof HTMLElement) {
(component as any).name = 'block-grid-2';
}
},
},
];
}
};
})
.observeRouteBuilder((routeBuilder) => {
this._catalogueRouteBuilder = routeBuilder;
});
}
render() {
return this._variantId
? html`<div>
umb-property-editor-ui-block-grid, inner routing test:
<uui-tab-group slot="navigation">
<uui-tab
label="TAB 1"
href="${this._routerPath + '/'}modal-1"
.active=${this._routerPath + '/' + 'modal-1' === this._activePath}></uui-tab>
<uui-tab
label="TAB 2"
href="${this._routerPath + '/'}modal-2"
.active=${this._routerPath + '/' + 'modal-2' === this._activePath}></uui-tab>
</uui-tab-group>
<umb-variant-router-slot
.variantId=${[this._variantId]}
id="router-slot"
.routes="${this._routes}"
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.localActiveViewPath;
}}>
</umb-variant-router-slot>
</div>`
: 'loading...';
if (this._blocks?.length === 1) {
const elementKey = this._blocks[0].contentElementTypeKey;
this._directRoute =
this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) + 'modal/umb-modal-workspace/create/' + elementKey;
}
return html`<uui-button-group>
<uui-button
id="add-button"
look="placeholder"
label=${this._createButtonLabel}
href=${this._directRoute ?? this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) ?? ''}></uui-button>
<uui-button
label=${this.localize.term('content_createFromClipboard')}
look="placeholder"
href=${this._catalogueRouteBuilder?.({ view: 'clipboard', index: -1 }) ?? ''}>
<uui-icon name="icon-paste-in"></uui-icon>
</uui-button>
</uui-button-group>`;
}
static styles = [UmbTextStyles];
static styles = [
UmbTextStyles,
css`
:host {
display: grid;
gap: 1px;
}
> div {
display: flex;
flex-direction: column;
align-items: stretch;
}
uui-button-group {
padding-top: 1px;
display: grid;
grid-template-columns: 1fr auto;
}
`,
];
}
export default UmbPropertyEditorUIBlockGridElement;

View File

@@ -15,6 +15,7 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/mod
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
export interface UmbBlockListLayoutModel extends UmbBlockLayoutBaseModel {}
@@ -45,6 +46,8 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
},
});
#catalogueModal: UmbModalRouteRegistrationController<typeof UMB_BLOCK_CATALOGUE_MODAL.DATA, undefined>;
private _value: UmbBlockListValueModel = {
layout: {},
contentData: [],
@@ -67,9 +70,13 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
this.#context.setSettings(buildUpValue.settingsData);
}
@state()
private _createButtonLabel = this.localize.term('content_createEmpty');
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
const validationLimit = config.getValueByAlias<NumberRangeValueType>('validationLimit');
this._limitMin = validationLimit?.min;
@@ -78,6 +85,13 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
const blocks = config.getValueByAlias<Array<UmbBlockTypeBaseModel>>('blocks') ?? [];
this.#context.setBlockTypes(blocks);
const customCreateButtonLabel = config.getValueByAlias<string>('createLabel');
if (customCreateButtonLabel) {
this._createButtonLabel = customCreateButtonLabel;
} else if (blocks.length === 1) {
this._createButtonLabel = `${this.localize.term('general_add')} ${blocks[0].label}`;
}
const useInlineEditingAsDefault = config.getValueByAlias<boolean>('useInlineEditingAsDefault');
this.#context.setInlineEditingMode(useInlineEditingAsDefault);
//config.useSingleBlockMode
@@ -97,16 +111,29 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
private _blocks?: Array<UmbBlockTypeBaseModel>;
@state()
_layouts: Array<UmbBlockLayoutBaseModel> = [];
private _layouts: Array<UmbBlockLayoutBaseModel> = [];
@state()
_catalogueRouteBuilder?: UmbModalRouteBuilder;
private _catalogueRouteBuilder?: UmbModalRouteBuilder;
@state()
private _directRoute?: string;
#context = new UmbBlockListManagerContext(this);
constructor() {
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => {
this.observe(
propertyContext?.alias,
(alias) => {
this.#catalogueModal.setUniquePathValue('propertyAlias', alias);
},
'observePropertyAlias',
);
});
// TODO: Prevent initial notification from these observes:
this.observe(this.#context.layouts, (layouts) => {
this._value = { ...this._value, layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS]: layouts } };
@@ -133,7 +160,8 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
this._blocks = blockTypes;
});
new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
.addUniquePaths(['propertyAlias'])
.addAdditionalPath(':view/:index')
.onSetup((routingInfo) => {
const index = routingInfo.index ? parseInt(routingInfo.index) : -1;
@@ -151,6 +179,11 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
}
render() {
if (this._blocks?.length === 1) {
const elementKey = this._blocks[0].contentElementTypeKey;
this._directRoute =
this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) + 'modal/umb-modal-workspace/create/' + elementKey;
}
return html` ${repeat(
this._layouts,
(x) => x.contentUdi,
@@ -164,10 +197,8 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
<uui-button
id="add-button"
look="placeholder"
label=${this.localize.term('content_createEmpty')}
href=${this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) ?? ''}>
${this.localize.term('content_createEmpty')}
</uui-button>
label=${this._createButtonLabel}
href=${this._directRoute ?? this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) ?? ''}></uui-button>
<uui-button
label=${this.localize.term('content_createFromClipboard')}
look="placeholder"

View File

@@ -2,10 +2,10 @@ import { UMB_BLOCK_WORKSPACE_MODAL } from '../../workspace/index.js';
import type {
UmbBlockCatalogueModalData,
UmbBlockCatalogueModalValue,
UmbBlockTypeGroup,
UmbBlockTypeWithGroupKey,
} from '@umbraco-cms/backoffice/block';
import { css, html, customElement, state, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { groupBy } from '@umbraco-cms/backoffice/external/lodash';
import {
UMB_MODAL_CONTEXT,
UmbModalBaseElement,
@@ -18,10 +18,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
UmbBlockCatalogueModalValue
> {
@state()
private _blocks: Array<UmbBlockTypeWithGroupKey> = [];
@state()
private _blockGroups: Array<{ key: string; name: string }> = [];
private _groupedBlocks: Array<{ name?: string; blocks: Array<UmbBlockTypeWithGroupKey> }> = [];
@state()
_openClipboard?: boolean;
@@ -55,16 +52,18 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
if (!this.data) return;
this._openClipboard = this.data.openClipboard ?? false;
this._blocks = this.data.blocks ?? [];
this._blockGroups = this.data.blockGroups ?? [];
}
/*
#onClickBlock(contentElementTypeKey: string) {
this.modalContext?.updateValue({ key: contentElementTypeKey });
this.modalContext?.submit();
const blocks: Array<UmbBlockTypeWithGroupKey> = this.data.blocks ?? [];
const blockGroups: Array<UmbBlockTypeGroup> = this.data.blockGroups ?? [];
const noGroupBlocks = blocks.filter((block) => !blockGroups.find((group) => group.key === block.groupKey));
const grouped = blockGroups.map((group) => ({
name: group.name ?? '',
blocks: blocks.filter((block) => block.groupKey === group.key),
}));
this._groupedBlocks = [{ blocks: noGroupBlocks }, ...grouped];
}
*/
render() {
return html`
@@ -87,17 +86,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
}
#renderCreateEmpty() {
const blockArrays = groupBy(this._blocks, 'groupKey');
const mappedGroupsAndBlocks = Object.entries(blockArrays).map(([key, value]) => {
const group = this._blockGroups.find((group) => group.key === key);
return { name: group?.name, blocks: value };
});
return html`
${mappedGroupsAndBlocks.map(
${this._groupedBlocks.map(
(group) => html`
${group.name ? html`<h2>${group.name}</h2>` : nothing}
${group.name ? html`<h4>${group.name}</h4>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
@@ -121,12 +113,18 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
#renderViews() {
return html`
<uui-tab-group slot="navigation">
<uui-tab label="Create Empty" ?active=${!this._openClipboard} @click=${() => (this._openClipboard = false)}>
Create Empty
<uui-tab
label=${this.localize.term('blockEditor_tabCreateEmpty')}
?active=${!this._openClipboard}
@click=${() => (this._openClipboard = false)}>
<umb-localize key=${this.localize.term('blockEditor_tabCreateEmpty')}>Create Empty</umb-localize>
<uui-icon slot="icon" name="icon-add"></uui-icon>
</uui-tab>
<uui-tab label="Clipboard" ?active=${this._openClipboard} @click=${() => (this._openClipboard = true)}>
Clipboard
<uui-tab
label=${this.localize.term('blockEditor_tabClipboard')}
?active=${this._openClipboard}
@click=${() => (this._openClipboard = true)}>
<umb-localize key=${this.localize.term('blockEditor_tabClipboard')}>Clipboard</umb-localize>
<uui-icon slot="icon" name="icon-paste-in"></uui-icon>
</uui-tab>
</uui-tab-group>

View File

@@ -1,9 +1,9 @@
import type { UmbBlockTypeBaseModel, UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block';
import type { UmbBlockTypeBaseModel, UmbBlockTypeGroup, UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockCatalogueModalData {
blocks: Array<UmbBlockTypeBaseModel>;
blockGroups?: Array<{ name: string; key: string }>;
blockGroups?: Array<UmbBlockTypeGroup>;
openClipboard?: boolean;
blockOriginData: UmbBlockWorkspaceData['originData'];
}

View File

@@ -6,21 +6,23 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type UmbTreePickerSource = {
type?: UmbTreePickerSourceType;
id?: string | null;
dynamicRoot?: UmbTreePickerDynamicRoot | null;
type: UmbTreePickerSourceType;
id?: string;
dynamicRoot?: UmbTreePickerDynamicRoot;
};
export type UmbTreePickerSourceType = 'content' | 'member' | 'media';
export type UmbTreePickerDynamicRoot = {
originAlias: string;
querySteps?: Array<UmbTreePickerDynamicRootQueryStep> | null;
originKey?: string;
querySteps?: Array<UmbTreePickerDynamicRootQueryStep>;
};
export type UmbTreePickerDynamicRootQueryStep = {
unique: string;
alias: string;
anyOfDocTypeKeys: Array<string>;
anyOfDocTypeKeys?: Array<string>;
};
@customElement('umb-input-tree-picker-source')
@@ -29,33 +31,33 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
return undefined;
}
private _type: UmbTreePickerSource['type'] = 'content';
#type: UmbTreePickerSourceType = 'content';
@property()
public set type(value: UmbTreePickerSource['type']) {
public set type(value: UmbTreePickerSourceType) {
if (value === undefined) {
value = this._type;
value = this.#type;
}
const oldValue = this._type;
const oldValue = this.#type;
this._options = this._options.map((option) =>
option.value === value ? { ...option, selected: true } : { ...option, selected: false },
);
this._type = value;
this.#type = value;
this.requestUpdate('type', oldValue);
}
public get type(): UmbTreePickerSource['type'] {
return this._type;
public get type(): UmbTreePickerSourceType {
return this.#type;
}
@property({ attribute: 'node-id' })
nodeId?: string | null;
nodeId?: string;
@property({ attribute: false })
dynamicRoot?: UmbTreePickerDynamicRoot | null;
dynamicRoot?: UmbTreePickerDynamicRoot | undefined;
@state()
_options: Array<Option> = [
@@ -64,20 +66,42 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
{ value: 'member', name: 'Members' },
];
#onTypeChange(event: UUISelectEvent) {
connectedCallback(): void {
super.connectedCallback();
// HACK: Workaround consolidating the old content-picker and dynamic-root. [LK:2024-01-24]
if (this.nodeId && !this.dynamicRoot) {
this.dynamicRoot = { originAlias: 'ByKey', originKey: this.nodeId, querySteps: [] };
}
}
#onContentTypeChange(event: UUISelectEvent) {
event.stopPropagation();
this.type = event.target.value as UmbTreePickerSource['type'];
this.type = event.target.value as UmbTreePickerSourceType;
this.nodeId = '';
this.nodeId = undefined;
this.dynamicRoot = undefined;
this.dispatchEvent(new UmbChangeEvent());
}
#onIdChange(event: CustomEvent) {
#onDocumentRootChange(event: CustomEvent) {
switch (this.type) {
case 'content':
this.nodeId = (<UmbInputDocumentPickerRootElement>event.target).unique;
this.dynamicRoot = (event.target as UmbInputDocumentPickerRootElement).data;
// HACK: Workaround consolidating the old content-picker and dynamic-root. [LK:2024-01-24]
if (this.dynamicRoot?.originAlias === 'ByKey') {
if (!this.dynamicRoot.querySteps || this.dynamicRoot.querySteps?.length === 0) {
this.nodeId = this.dynamicRoot.originKey;
} else {
this.nodeId = undefined;
}
} else if (this.nodeId) {
this.nodeId = undefined;
}
break;
case 'media':
case 'member':
@@ -85,20 +109,20 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
break;
}
this.dispatchEvent(new CustomEvent(event.type));
this.dispatchEvent(new UmbChangeEvent());
}
render() {
return html`<umb-input-dropdown-list
.options=${this._options}
@change="${this.#onTypeChange}"></umb-input-dropdown-list>
${this.#renderType()}`;
@change="${this.#onContentTypeChange}"
.options=${this._options}></umb-input-dropdown-list>
${this.#renderSourcePicker()}`;
}
#renderType() {
#renderSourcePicker() {
switch (this.type) {
case 'content':
return this.#renderTypeContent();
return this.#renderDocumentSourcePicker();
case 'media':
case 'member':
default:
@@ -106,10 +130,10 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
}
}
#renderTypeContent() {
#renderDocumentSourcePicker() {
return html`<umb-input-document-picker-root
@change=${this.#onIdChange}
.nodeId=${this.nodeId}></umb-input-document-picker-root>`;
@change=${this.#onDocumentRootChange}
.data=${this.dynamicRoot}></umb-input-document-picker-root>`;
}
static styles = [

View File

@@ -0,0 +1,25 @@
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestDynamicRootOrigin extends ManifestBase {
type: 'dynamicRootOrigin';
meta: MetaDynamicRootOrigin;
}
export interface ManifestDynamicRootQueryStep extends ManifestBase {
type: 'dynamicRootQueryStep';
meta: MetaDynamicRootQueryStep;
}
export interface MetaDynamicRootOrigin {
originAlias: string;
label?: string;
description?: string;
icon?: string;
}
export interface MetaDynamicRootQueryStep {
queryStepAlias: string;
label?: string;
description?: string;
icon?: string;
}

View File

@@ -3,6 +3,7 @@ import type { ManifestCollection } from './collection.models.js';
import type { ManifestCollectionView } from './collection-view.model.js';
import type { ManifestDashboard } from './dashboard.model.js';
import type { ManifestDashboardCollection } from './dashboard-collection.model.js';
import type { ManifestDynamicRootOrigin, ManifestDynamicRootQueryStep } from './dynamic-root.model.js';
import type { ManifestEntityAction } from './entity-action.model.js';
import type { ManifestEntityBulkAction } from './entity-bulk-action.model.js';
import type { ManifestExternalLoginProvider } from './external-login-provider.model.js';
@@ -47,6 +48,7 @@ export type * from './collection-action.model.js';
export type * from './collection-view.model.js';
export type * from './dashboard-collection.model.js';
export type * from './dashboard.model.js';
export type * from './dynamic-root.model.js';
export type * from './entity-action.model.js';
export type * from './entity-bulk-action.model.js';
export type * from './external-login-provider.model.js';
@@ -87,6 +89,8 @@ export type ManifestTypes =
| ManifestCollectionAction
| ManifestDashboard
| ManifestDashboardCollection
| ManifestDynamicRootOrigin
| ManifestDynamicRootQueryStep
| ManifestEntityAction
| ManifestEntityBulkAction
| ManifestEntryPoint

View File

@@ -37,8 +37,9 @@ export class UmbPropertyEditorUITreePickerSourcePickerElement
render() {
return html`<umb-input-tree-picker-source
@change=${this.#onChange}
.type=${this.value?.type}
.nodeId=${this.value?.id}></umb-input-tree-picker-source>`;
.type=${this.value?.type ?? 'content'}
.nodeId=${this.value?.id}
.dynamicRoot=${this.value?.dynamicRoot}></umb-input-tree-picker-source>`;
}
static styles = [UmbTextStyles];

View File

@@ -50,8 +50,10 @@ export class UmbPropertyEditorUITreePickerSourceTypePickerElement
this.observe(
await this.#datasetContext.propertyValueByAlias('startNode'),
(value) => {
if (!value) return;
const startNode = value as UmbTreePickerSource;
if (startNode.type) {
if (startNode?.type) {
// If we had a sourceType before, we can see this as a change and not the initial value,
// so let's reset the value, so we don't carry over content-types to the new source type.
if (this.#initialized && this.sourceType !== startNode.type) {

View File

@@ -1,7 +1,10 @@
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { type UmbPropertyEditorConfigCollection, UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UmbDynamicRootRepository } from '@umbraco-cms/backoffice/dynamic-root';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree';
import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
@@ -16,7 +19,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
value = '';
@state()
type?: UmbTreePickerSource['type'];
type: UmbTreePickerSource['type'] = 'content';
@state()
startNodeId?: string | null;
@@ -36,12 +39,19 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
@state()
ignoreUserStartNodes?: boolean;
#dynamicRoot?: UmbTreePickerSource['dynamicRoot'];
#dynamicRootRepository = new UmbDynamicRootRepository(this);
#workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE;
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
const startNode: UmbTreePickerSource | undefined = config?.getValueByAlias('startNode');
if (startNode) {
this.type = startNode.type;
this.startNodeId = startNode.id;
this.#dynamicRoot = startNode.dynamicRoot;
}
this.min = Number(config?.getValueByAlias('minNumber')) || 0;
@@ -52,6 +62,34 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
this.ignoreUserStartNodes = config?.getValueByAlias('ignoreUserStartNodes');
}
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#workspaceContext = workspaceContext;
});
}
connectedCallback() {
super.connectedCallback();
this.#setStartNodeId();
}
async #setStartNodeId() {
if (this.startNodeId) return;
const entityId = this.#workspaceContext?.getEntityId();
// TODO: Awaiting the workspace context to have a parent entity ID value. [LK]
// e.g. const parentEntityId = this.#workspaceContext?.getParentEntityId();
if (entityId && this.#dynamicRoot) {
const result = await this.#dynamicRootRepository.postDynamicRootQuery(this.#dynamicRoot, entityId);
if (result && result.length > 0) {
this.startNodeId = result[0];
}
}
}
#onChange(e: CustomEvent) {
this.value = (e.target as UmbInputTreeElement).value as string;
this.dispatchEvent(new UmbPropertyValueChangeEvent());

View File

@@ -13,7 +13,7 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
return undefined;
}
private _type: UmbTreePickerSource['type'] = undefined;
private _type: UmbTreePickerSource['type'] = 'content';
@property()
public set type(newType: UmbTreePickerSource['type']) {
const oldType = this._type;

View File

@@ -1,101 +1,260 @@
import { UmbDocumentPickerContext } from '../input-document/input-document.context.js';
import type { UmbDocumentItemModel } from '../../repository/index.js';
import { html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { html, css, customElement, property, ifDefined, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import {
UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL,
UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL,
} from '@umbraco-cms/backoffice/dynamic-root';
import type {
ManifestDynamicRootOrigin,
ManifestDynamicRootQueryStep,
} from '@umbraco-cms/backoffice/extension-registry';
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
import type { UmbTreePickerDynamicRoot, UmbTreePickerDynamicRootQueryStep } from '@umbraco-cms/backoffice/components';
@customElement('umb-input-document-picker-root')
export class UmbInputDocumentPickerRootElement extends FormControlMixin(UmbLitElement) {
public get unique(): string | null | undefined {
return this.#documentPickerContext.getSelection()[0];
}
public set unique(unique: string | null | undefined) {
const selection = unique ? [unique] : [];
this.#documentPickerContext.setSelection(selection);
}
@property()
public set value(unique: string) {
this.unique = unique;
}
@state()
private _items?: Array<UmbDocumentItemModel>;
#documentPickerContext = new UmbDocumentPickerContext(this);
// TODO: DynamicRoot - once feature implemented, wire up context and picker UI. [LK]
#dynamicRootPickerContext = {
openPicker: () => {
throw new Error('DynamicRoot picker has not been implemented yet.');
},
};
constructor() {
super();
this.#documentPickerContext.max = 1;
this.observe(this.#documentPickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#documentPickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected getFormElement() {
return undefined;
}
@state()
private _originManifests: Array<ManifestDynamicRootOrigin> = [];
@state()
private _queryStepManifests: Array<ManifestDynamicRootQueryStep> = [];
@property({ attribute: false })
data?: UmbTreePickerDynamicRoot | undefined;
#dynamicRootOrigin?: { label: string; icon: string; description?: string };
#modalContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
#openModal?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
this.observe(
umbExtensionsRegistry.byType('dynamicRootOrigin'),
(originManifests: Array<ManifestDynamicRootOrigin>) => {
this._originManifests = originManifests;
},
);
this.observe(
umbExtensionsRegistry.byType('dynamicRootQueryStep'),
(queryStepManifests: Array<ManifestDynamicRootQueryStep>) => {
this._queryStepManifests = queryStepManifests;
},
);
}
connectedCallback(): void {
super.connectedCallback();
this.#updateDynamicRootOrigin(this.data);
this.#updateDynamicRootQuerySteps(this.data?.querySteps);
}
#sorter = new UmbSorterController<UmbTreePickerDynamicRootQueryStep>(this, {
getUniqueOfElement: (element) => {
return element.id;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.unique;
},
identifier: 'Umb.SorterIdentifier.InputDocumentPickerRoot',
itemSelector: 'uui-ref-node',
containerSelector: '#query-steps',
onChange: ({ model }) => {
if (this.data && this.data.querySteps) {
const querySteps = model;
this.#updateDynamicRootQuerySteps(querySteps);
this.dispatchEvent(new UmbChangeEvent());
}
},
});
#openDynamicRootOriginPicker() {
this.#openModal = this.#modalContext?.open(UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL, {});
this.#openModal?.onSubmit().then((data: UmbTreePickerDynamicRoot) => {
const existingData = { ...this.data };
existingData.originKey = undefined;
this.data = { ...existingData, ...data };
this.#updateDynamicRootOrigin(this.data);
this.dispatchEvent(new UmbChangeEvent());
});
}
#openDynamicRootQueryStepPicker() {
this.#openModal = this.#modalContext?.open(UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL, {});
this.#openModal?.onSubmit().then((step) => {
if (this.data) {
const querySteps = [...(this.data.querySteps ?? []), step];
this.#updateDynamicRootQuerySteps(querySteps);
this.dispatchEvent(new UmbChangeEvent());
}
});
}
#updateDynamicRootOrigin(data?: UmbTreePickerDynamicRoot) {
if (!data) return;
const origin = this._originManifests.find((item) => item.meta.originAlias === data.originAlias)?.meta;
this.#dynamicRootOrigin = {
label: origin?.label ?? data.originAlias,
icon: origin?.icon ?? 'icon-wand',
description: data.originKey,
};
}
#updateDynamicRootQuerySteps(querySteps?: Array<UmbTreePickerDynamicRootQueryStep>) {
if (!this.data) return;
if (querySteps) {
// NOTE: Ensure that the `unique` ID is populated for each query step. [LK]
querySteps = querySteps.map((item) => (item.unique ? item : { ...item, unique: UmbId.new() }));
}
this.#sorter?.setModel(querySteps ?? []);
this.data = { ...this.data, ...{ querySteps } };
}
#getQueryStepMeta(item: UmbTreePickerDynamicRootQueryStep): {
unique: string;
label: string;
icon: string;
description?: string;
} {
const step = this._queryStepManifests.find((step) => step.meta.queryStepAlias === item.alias)?.meta;
const docTypes = item.anyOfDocTypeKeys?.join(', ');
const description = docTypes ? this.localize.term('dynamicRoot_queryStepTypes') + docTypes : undefined;
return {
unique: item.unique,
label: step?.label ?? item.alias,
icon: step?.icon ?? 'icon-lab',
description,
};
}
#removeDynamicRootQueryStep(item: UmbTreePickerDynamicRootQueryStep) {
if (this.data?.querySteps) {
const index = this.data.querySteps.indexOf(item);
if (index !== -1) {
const querySteps = [...this.data.querySteps];
querySteps.splice(index, 1);
this.#updateDynamicRootQuerySteps(querySteps);
this.dispatchEvent(new UmbChangeEvent());
}
}
}
#clearDynamicRootQuery() {
this.data = undefined;
this.#dynamicRootOrigin = undefined;
this.dispatchEvent(new UmbChangeEvent());
}
render() {
return html`
${this._items
? html` <uui-ref-list
>${repeat(
this._items,
(item) => item.unique,
(item) => this._renderItem(item),
)}
</uui-ref-list>`
: ''}
${this.#renderButtons()}
${this.#renderAddOriginButton()}
<uui-ref-list>${this.#renderOrigin()}</uui-ref-list>
<uui-ref-list id="query-steps">${this.#renderQuerySteps()}</uui-ref-list>
${this.#renderAddQueryStepButton()} ${this.#renderClearButton()}
`;
}
#renderButtons() {
if (this.unique) return;
//TODO: Dynamic paths
return html` <uui-button-group>
#renderAddOriginButton() {
if (this.data?.originAlias) return;
return html`
<uui-button
look="placeholder"
@click=${() => this.#documentPickerContext.openPicker()}
label=${this.localize.term('contentPicker_defineRootNode')}></uui-button>
<uui-button
look="placeholder"
@click=${() => this.#dynamicRootPickerContext.openPicker()}
label=${this.localize.term('contentPicker_defineDynamicRoot')}></uui-button>
</uui-button-group>`;
class="add-button"
@click=${this.#openDynamicRootOriginPicker}
label=${this.localize.term('contentPicker_defineDynamicRoot')}
look="placeholder"></uui-button>
`;
}
private _renderItem(item: UmbDocumentItemModel) {
if (!item.unique) return;
// TODO: get correct variant name
const name = item.variants[0]?.name;
#renderOrigin() {
if (!this.#dynamicRootOrigin) return;
return html`
<uui-ref-node name=${name} detail=${ifDefined(item.unique)}>
<!-- TODO: implement is trashed <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> -->
<uui-ref-node
border
standalone
name=${this.#dynamicRootOrigin.label}
detail=${ifDefined(this.#dynamicRootOrigin.description)}>
<uui-icon slot="icon" name=${ifDefined(this.#dynamicRootOrigin.icon)}></uui-icon>
<uui-action-bar slot="actions">
<uui-button @click=${() => this.#documentPickerContext.openPicker()} label="Edit document ${name}"
>Edit</uui-button
>
<uui-button
@click=${() => this.#documentPickerContext.requestRemoveItem(item.unique)}
label="Remove document ${name}"
>Remove</uui-button
>
@click=${this.#openDynamicRootOriginPicker}
label="${this.localize.term('general_edit')}"></uui-button>
</uui-action-bar>
</uui-ref-node>
`;
}
#renderClearButton() {
if (!this.#dynamicRootOrigin) return;
return html`
<uui-button @click=${this.#clearDynamicRootQuery}>${this.localize.term('buttons_clearSelection')}</uui-button>
`;
}
#renderQuerySteps() {
if (!this.data?.querySteps) return;
return repeat(
this.data.querySteps,
(item) => item.unique,
(item) => this.#renderQueryStep(item),
);
}
#renderQueryStep(item: UmbTreePickerDynamicRootQueryStep) {
if (!item.alias) return;
const step = this.#getQueryStepMeta(item);
return html`
<uui-ref-node border standalone id=${step.unique} name=${step.label} detail="${ifDefined(step.description)}">
<uui-icon slot="icon" name=${step.icon}></uui-icon>
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#removeDynamicRootQueryStep(item)}
label=${this.localize.term('general_remove')}></uui-button>
</uui-action-bar>
</uui-ref-node>
`;
}
#renderAddQueryStepButton() {
if (!this.#dynamicRootOrigin) return;
return html` <uui-button
class="add-button"
@click=${this.#openDynamicRootQueryStepPicker}
label=${this.localize.term('dynamicRoot_addQueryStep')}
look="placeholder"></uui-button>`;
}
static styles = [
css`
.add-button {
width: 100%;
}
uui-ref-node[drag-placeholder] {
opacity: 0.2;
}
`,
];
}
export default UmbInputDocumentPickerRootElement;

View File

@@ -121,6 +121,13 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
this._editDocumentPath = routeBuilder({});
});
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
connectedCallback(): void {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
@@ -132,9 +139,6 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
() => this.maxMessage,
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
);
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected getFormElement() {
@@ -150,7 +154,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
#openPicker() {
// TODO: Configure the content picker, with `startNodeId` and `ignoreUserStartNodes` [LK]
console.log('_openPicker', [this.startNodeId, this.ignoreUserStartNodes]);
console.log('#openPicker', [this.startNodeId, this.ignoreUserStartNodes]);
this.#pickerContext.openPicker({
hideTreeRoot: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -0,0 +1,2 @@
export * from './modals/index.js';
export * from './repository/index.js';

View File

@@ -0,0 +1,7 @@
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
export const manifests = [
...modalManifests,
...repositoryManifests,
];

View File

@@ -0,0 +1,109 @@
import { UmbDocumentPickerContext } from '../../documents/documents/components/input-document/input-document.context.js';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { css, html, customElement, map, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestDynamicRootOrigin } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbTreePickerDynamicRoot } from '@umbraco-cms/backoffice/components';
@customElement('umb-dynamic-root-origin-picker-modal')
export class UmbDynamicRootOriginPickerModalModalElement extends UmbModalBaseElement {
@state()
private _origins: Array<ManifestDynamicRootOrigin> = [];
#documentPickerContext = new UmbDocumentPickerContext(this);
constructor() {
super();
this.#documentPickerContext.max = 1;
this.observe(umbExtensionsRegistry.byType('dynamicRootOrigin'), (origins: Array<ManifestDynamicRootOrigin>) => {
this._origins = origins;
});
}
#choose(item: ManifestDynamicRootOrigin) {
switch (item.meta.originAlias) {
// NOTE: Edge-case. Currently this is the only one that uses a document picker,
// but other custom origins may want other configuration options. [LK:2024-01-25]
case 'ByKey':
this.#openDocumentPicker(item.meta.originAlias);
break;
default:
this.#submit({ originAlias: item.meta.originAlias });
break;
}
}
#close() {
this.modalContext?.reject();
}
async #openDocumentPicker(originAlias: string) {
await this.#documentPickerContext.openPicker({
hideTreeRoot: true,
});
const selectedItems = this.#documentPickerContext.getSelection();
if (selectedItems.length !== 1) return;
this.#submit({
originAlias,
originKey: selectedItems[0],
});
}
#submit(value: UmbTreePickerDynamicRoot) {
this.modalContext?.setValue(value);
this.modalContext?.submit();
}
render() {
return html`
<umb-body-layout headline="${this.localize.term('dynamicRoot_pickDynamicRootOriginTitle')}">
<div id="main">
<uui-box>
${map(
this._origins,
(item) => html`
<uui-button @click=${() => this.#choose(item)} look="placeholder" label="${ifDefined(item.meta.label)}">
<h3>${item.meta.label}</h3>
<p>${item.meta.description}</p>
</uui-button>
`,
)}
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="default" label="${this.localize.term('general_close')}"></uui-button>
</div>
</umb-body-layout>
`;
}
static styles = [
css`
uui-box > uui-button {
display: block;
--uui-button-content-align: flex-start;
}
uui-box > uui-button:not(:last-of-type) {
margin-bottom: var(--uui-size-space-5);
}
h3,
p {
text-align: left;
}
`,
];
}
export default UmbDynamicRootOriginPickerModalModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dynamic-root-origin-picker-modal': UmbDynamicRootOriginPickerModalModalElement;
}
}

View File

@@ -0,0 +1,105 @@
import { UmbDocumentTypePickerContext } from '../../documents/document-types/components/input-document-type/input-document-type.context.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, map, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbTreePickerDynamicRootQueryStep } from '@umbraco-cms/backoffice/components';
import type { ManifestDynamicRootQueryStep } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-dynamic-root-query-step-picker-modal')
export class UmbDynamicRootQueryStepPickerModalModalElement extends UmbModalBaseElement {
@state()
private _querySteps: Array<ManifestDynamicRootQueryStep> = [];
#documentTypePickerContext = new UmbDocumentTypePickerContext(this);
constructor() {
super();
this.observe(
umbExtensionsRegistry.byType('dynamicRootQueryStep'),
(querySteps: Array<ManifestDynamicRootQueryStep>) => {
this._querySteps = querySteps;
},
);
}
#choose(item: ManifestDynamicRootQueryStep) {
this.#openDocumentTypePicker(item.meta.queryStepAlias);
}
#close() {
this.modalContext?.reject();
}
async #openDocumentTypePicker(alias: string) {
await this.#documentTypePickerContext.openPicker({
hideTreeRoot: true,
pickableFilter: (x) => x.isElement === false,
});
const selectedItems = this.#documentTypePickerContext.getSelection();
this.#submit({
unique: UmbId.new(),
alias: alias,
anyOfDocTypeKeys: selectedItems,
});
}
#submit(value: UmbTreePickerDynamicRootQueryStep) {
this.modalContext?.setValue(value);
this.modalContext?.submit();
}
render() {
return html`
<umb-body-layout headline="${this.localize.term('dynamicRoot_pickDynamicRootQueryStepTitle')}">
<div id="main">
<uui-box>
${map(
this._querySteps,
(item) => html`
<uui-button @click=${() => this.#choose(item)} look="placeholder" label="${ifDefined(item.meta.label)}">
<h3>${item.meta.label}</h3>
<p>${item.meta.description}</p>
</uui-button>
`,
)}
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="default" label="${this.localize.term('general_close')}"></uui-button>
</div>
</umb-body-layout>
`;
}
static styles = [
UmbTextStyles,
css`
uui-box > uui-button {
display: block;
--uui-button-content-align: flex-start;
}
uui-box > uui-button:not(:last-of-type) {
margin-bottom: var(--uui-size-space-5);
}
h3,
p {
text-align: left;
}
`,
];
}
export default UmbDynamicRootQueryStepPickerModalModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dynamic-root-query-step-picker-modal': UmbDynamicRootQueryStepPickerModalModalElement;
}
}

View File

@@ -0,0 +1,22 @@
import {
UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL_ALIAS,
UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL_ALIAS,
} from './manifests.js';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export const UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL = new UmbModalToken(UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL_ALIAS, {
modal: {
type: 'sidebar',
size: 'small',
},
});
export const UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL = new UmbModalToken(
UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL_ALIAS,
{
modal: {
type: 'sidebar',
size: 'small',
},
},
);

View File

@@ -0,0 +1,139 @@
import type {
ManifestDynamicRootOrigin,
ManifestDynamicRootQueryStep,
ManifestModal,
} from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL_ALIAS = 'Umb.Modal.DynamicRoot.OriginPicker';
export const UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL_ALIAS = 'Umb.Modal.DynamicRoot.QueryStepPicker';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: UMB_DYNAMIC_ROOT_ORIGIN_PICKER_MODAL_ALIAS,
name: 'Choose an origin',
js: () => import('./dynamic-root-origin-picker-modal.element.js'),
},
{
type: 'modal',
alias: UMB_DYNAMIC_ROOT_QUERY_STEP_PICKER_MODAL_ALIAS,
name: 'Append step to query',
js: () => import('./dynamic-root-query-step-picker-modal.element.js'),
},
];
const origins: Array<ManifestDynamicRootOrigin> = [
{
type: 'dynamicRootOrigin',
alias: 'Umb.DynamicRootOrigin.Root',
name: 'Dynamic Root Origin: Root',
meta: {
originAlias: 'Root',
label: 'Root',
description: 'Root node of this editing session.',
icon: 'icon-home',
},
weight: 100,
},
{
type: 'dynamicRootOrigin',
alias: 'Umb.DynamicRootOrigin.Parent',
name: 'Dynamic Root Origin: Parent',
meta: {
originAlias: 'Parent',
label: 'Parent',
description: 'The parent node of the source in this editing session.',
icon: 'icon-page-up',
},
weight: 90,
},
{
type: 'dynamicRootOrigin',
alias: 'Umb.DynamicRootOrigin.Current',
name: 'Dynamic Root Origin: Current',
meta: {
originAlias: 'Current',
label: 'Current',
description: 'The content node that is source for this editing session.',
icon: 'icon-document',
},
weight: 80,
},
{
type: 'dynamicRootOrigin',
alias: 'Umb.DynamicRootOrigin.Site',
name: 'Dynamic Root Origin: Site',
meta: {
originAlias: 'Site',
label: 'Site',
description: 'Find nearest node with a hostname.',
icon: 'icon-home',
},
weight: 70,
},
{
type: 'dynamicRootOrigin',
alias: 'Umb.DynamicRootOrigin.ByKey',
name: 'Dynamic Root Origin: By Key',
meta: {
originAlias: 'ByKey',
label: 'Specific Node',
description: 'Pick a specific Node as the origin for this query.',
icon: 'icon-wand',
},
weight: 60,
},
];
const querySteps: Array<ManifestDynamicRootQueryStep> = [
{
type: 'dynamicRootQueryStep',
alias: 'Umb.DynamicRootQueryStep.NearestAncestorOrSelf',
name: 'Dynamic Root Query Step: Nearest Ancestor Or Self',
meta: {
queryStepAlias: 'NearestAncestorOrSelf',
label: 'Nearest Ancestor Or Self',
description: 'Query the nearest ancestor or self that fits with one of the configured types.',
icon: 'icon-arrow-up',
},
weight: 100,
},
{
type: 'dynamicRootQueryStep',
alias: 'Umb.DynamicRootQueryStep.FurthestAncestorOrSelf',
name: 'Dynamic Root Query Step: Furthest Ancestor Or Self',
meta: {
queryStepAlias: 'FurthestAncestorOrSelf',
label: 'Furthest Ancestor Or Self',
description: 'Query the furthest ancestor or self that fits with one of the configured types.',
icon: 'icon-arrow-up',
},
weight: 90,
},
{
type: 'dynamicRootQueryStep',
alias: 'Umb.DynamicRootQueryStep.NearestDescendantOrSelf',
name: 'Dynamic Root Query Step: Nearest Descendant Or Self',
meta: {
queryStepAlias: 'NearestDescendantOrSelf',
label: 'Nearest Descendant Or Self',
description: 'Query the nearest descendant or self that fits with one of the configured types.',
icon: 'icon-arrow-down',
},
weight: 80,
},
{
type: 'dynamicRootQueryStep',
alias: 'Umb.DynamicRootQueryStep.FurthestDescendantOrSelf',
name: 'Dynamic Root Query Step: Furthest Descendant Or Self',
meta: {
queryStepAlias: 'FurthestDescendantOrSelf',
label: 'Furthest Descendant Or Self',
description: 'Query the furthest descendant or self that fits with one of the configured types.',
icon: 'icon-arrow-down',
},
weight: 70,
},
];
export const manifests = [...modals, ...origins, ...querySteps];

View File

@@ -0,0 +1,42 @@
import { UmbDynamicRootServerDataSource } from './dynamic-root.server.data.js';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { DynamicRootRequestModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbTreePickerDynamicRoot } from '@umbraco-cms/backoffice/components';
const GUID_EMPTY: string = '00000000-0000-0000-0000-000000000000';
export class UmbDynamicRootRepository extends UmbBaseController {
#dataSource: UmbDynamicRootServerDataSource;
constructor(host: UmbControllerHostElement) {
super(host);
this.#dataSource = new UmbDynamicRootServerDataSource(host);
}
async postDynamicRootQuery(query: UmbTreePickerDynamicRoot, entityId: string, parentId?: string) {
const model: DynamicRootRequestModel = {
context: {
id: entityId,
parentId: parentId ?? GUID_EMPTY,
},
query: {
origin: {
alias: query.originAlias,
key: query.originKey,
},
steps: query.querySteps!.map((step) => {
return {
alias: step.alias!,
documentTypeIds: step.anyOfDocTypeKeys!,
};
}),
},
};
const result = await this.#dataSource.postDynamicRootQuery(model);
return result?.roots;
}
}

View File

@@ -0,0 +1,26 @@
import { DynamicRootResource } from '@umbraco-cms/backoffice/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { DynamicRootRequestModel, DynamicRootResponseModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDynamicRootServerDataSource {
#host: UmbControllerHost;
constructor(host: UmbControllerHost) {
this.#host = host;
}
async postDynamicRootQuery(args: DynamicRootRequestModel): Promise<DynamicRootResponseModel | undefined> {
if (!args.context) throw new Error('Dynamic Root context is missing');
if (!args.query) throw new Error('Dynamic Root query is missing');
const requestBody: DynamicRootRequestModel = {
context: args.context,
query: args.query,
};
const { data } = await tryExecuteAndNotify(this.#host, DynamicRootResource.postDynamicRootQuery({ requestBody }));
return data;
}
}

View File

@@ -0,0 +1,2 @@
export { UMB_DYNAMIC_ROOT_REPOSITORY_ALIAS } from './manifests.js';
export { UmbDynamicRootRepository } from './dynamic-root.repository.js';

View File

@@ -0,0 +1,3 @@
export const UMB_DYNAMIC_ROOT_REPOSITORY_ALIAS = 'Umb.Repository.DynamicRoot';
export const manifests = [];

View File

@@ -0,0 +1,9 @@
export const name = 'Umbraco.Core.DynamicRoot';
export const extensions = [
{
name: 'Dynamic Root Bundle',
alias: 'Umb.Bundle.DynamicRoot',
type: 'bundle',
js: () => import('./manifests.js'),
},
];

View File

@@ -92,6 +92,7 @@
"@umbraco-cms/backoffice/data-type": ["./src/packages/core/data-type/index.ts"],
"@umbraco-cms/backoffice/language": ["src/packages/language/index.ts"],
"@umbraco-cms/backoffice/dynamic-root": ["./src/packages/dynamic-root/index.ts"],
"@umbraco-cms/backoffice/logviewer": ["src/packages/log-viewer/index.ts"],
"@umbraco-cms/backoffice/relation-types": ["src/packages/relations/relation-types/index.ts"],
"@umbraco-cms/backoffice/relations": ["src/packages/relations/relations/index.ts"],

View File

@@ -108,6 +108,7 @@ export default {
'@umbraco-cms/backoffice/language': './src/packages/language/index.ts',
'@umbraco-cms/backoffice/data-type': './src/packages/core/data-type/index.ts',
'@umbraco-cms/backoffice/dynamic-root': './src/packages/dynamic-root/index.ts',
'@umbraco-cms/backoffice/logviewer': './src/packages/settings/logviewer/index.ts',
'@umbraco-cms/backoffice/relation-type': './src/packages/relations/relation-types/index.ts',
'@umbraco-cms/backoffice/relation': './src/packages/relations/relations/index.ts',