Merge branch 'main' into v17/dev

This commit is contained in:
Nikolaj Geisle
2025-10-08 19:18:04 +02:00
10 changed files with 178 additions and 60 deletions

View File

@@ -11,10 +11,7 @@ export class UmbBlockWorkspaceEditorElement extends UmbLitElement {
this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => {
if (context) {
this.observe(
observeMultiple([
context.isNew,
context.content.structure.ownerContentTypeObservablePart((contentType) => contentType?.name),
]),
observeMultiple([context.isNew, context.name]),
([isNew, name]) => {
this._headline =
this.localize.term(isNew ? 'general_add' : 'general_edit') + ' ' + this.localize.string(name);

View File

@@ -1,5 +1,5 @@
import type { UmbBlockDataModel, UmbBlockLayoutBaseModel } from '../types.js';
import { UMB_BLOCK_ENTRIES_CONTEXT, UMB_BLOCK_ENTRY_CONTEXT, UMB_BLOCK_MANAGER_CONTEXT } from '../context/index.js';
import type { UmbBlockDataModel, UmbBlockDataValueModel, UmbBlockLayoutBaseModel } from '../types.js';
import { UMB_BLOCK_ENTRIES_CONTEXT, UMB_BLOCK_MANAGER_CONTEXT } from '../context/index.js';
import { UmbBlockWorkspaceEditorElement } from './block-workspace-editor.element.js';
import { UmbBlockElementManager } from './block-element-manager.js';
import type { UmbBlockWorkspaceOriginData } from './block-workspace.modal-token.js';
@@ -24,6 +24,7 @@ import { decodeFilePath, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/bac
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import { UmbUfmVirtualRenderController } from '@umbraco-cms/backoffice/ufm';
export type UmbBlockWorkspaceElementManagerNames = 'content' | 'settings';
@@ -66,6 +67,8 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
#name = new UmbStringState<string | undefined>(undefined);
readonly name = this.#name.asObservable();
#labelRender = new UmbUfmVirtualRenderController(this);
#variantId = new UmbClassState<UmbVariantId | undefined>(undefined);
readonly variantId = this.#variantId.asObservable();
@@ -99,14 +102,22 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
this.#blockEntries = context;
}).asPromise({ preventTimeout: true });
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
this.#name.setValue(context?.getName());
});
this.observe(
this.variantId,
(variantId) => {
this.content.setVariantId(variantId);
this.settings.setVariantId(variantId);
},
null,
);
this.observe(this.variantId, (variantId) => {
this.content.setVariantId(variantId);
this.settings.setVariantId(variantId);
});
this.observe(
observeMultiple([this.content.values, this.settings.values]),
async ([contentValues]) => {
this.#renderLabel(contentValues);
},
'observeContentForLabelRender',
);
this.routes.setRoutes([
{
@@ -210,15 +221,17 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
(contentTypeId) => {
this.observe(
contentTypeId ? manager.blockTypeOf(contentTypeId) : undefined,
(blockType) => {
if (!blockType?.editorSize) return;
const editorConfig = manager.getEditorConfiguration();
const useInlineEditing = editorConfig?.find((x) => x.alias === 'useInlineEditingAsDefault')?.value;
if (!useInlineEditing) {
this.setEditorSize(blockType.editorSize);
async (blockType) => {
if (blockType?.editorSize) {
const editorConfig = manager.getEditorConfiguration();
const useInlineEditing = editorConfig?.find((x) => x.alias === 'useInlineEditingAsDefault')?.value;
if (!useInlineEditing) {
this.setEditorSize(blockType.editorSize);
}
}
await this.content.structure.whenLoaded();
this.#gotLabel(blockType?.label ?? this.content.structure.getOwnerContentTypeName());
},
'observeBlockType',
);
@@ -227,6 +240,29 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
);
}
#gotLabel(label: string | undefined) {
if (label) {
this.#labelRender.markdown = label;
this.#renderLabel(this.content.getValues());
}
}
async #renderLabel(contentValues: Array<UmbBlockDataValueModel> | undefined) {
const valueObject = {} as Record<string, unknown>;
if (contentValues) {
for (const property of contentValues) {
valueObject[property.alias] = property.value;
}
}
this.#labelRender.value = valueObject;
// Await one animation frame:
await new Promise((resolve) => requestAnimationFrame(() => resolve(true)));
const result = this.#labelRender.toString();
this.#name.setValue(result);
this.view.setTitle(result);
}
#allowNavigateAway = false;
#onWillNavigate = async (e: CustomEvent) => {
const newUrl = e.detail.url;

View File

@@ -39,7 +39,7 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
return this.#legacyMergedChildContainers.asObservable();
}
#childContainersMerged = new UmbArrayState<UmbPropertyTypeContainerMergedModel>([], (x) => x.path);
#childContainersMerged = new UmbArrayState<UmbPropertyTypeContainerMergedModel>([], (x) => x.key);
public readonly childContainers = this.#childContainersMerged.asObservable();
// Owner containers are containers owned by the owner Content Type (The specific one up for editing)

View File

@@ -198,6 +198,8 @@ export class UmbContentTypeStructureManager<
this.#ownerContentTypeUnique = unique;
if (!unique) {
this.#initRejection?.(`Content Type structure manager could not load: ${unique}`);
this.#initResolver = undefined;
this.#initRejection = undefined;
return Promise.reject(
new Error('The unique identifier is missing. A valid unique identifier is required to load the content type.'),
);
@@ -207,6 +209,8 @@ export class UmbContentTypeStructureManager<
const result = await this.observe(observable).asPromise();
if (!result) {
this.#initRejection?.(`Content Type structure manager could not load: ${unique}`);
this.#initResolver = undefined;
this.#initRejection = undefined;
return {
error: new UmbError(`Content Type structure manager could not load: ${unique}`),
asObservable: () => observable,
@@ -219,10 +223,14 @@ export class UmbContentTypeStructureManager<
}).catch(() => {
const msg = `Content Type structure manager could not load: ${unique}. Not all Content Types loaded successfully.`;
this.#initRejection?.(msg);
this.#initResolver = undefined;
this.#initRejection = undefined;
return Promise.reject(new UmbError(msg));
});
this.#initResolver?.(result);
this.#initResolver = undefined;
this.#initRejection = undefined;
return { data: result, asObservable: () => this.ownerContentType };
}
@@ -234,6 +242,8 @@ export class UmbContentTypeStructureManager<
const { data } = repsonse;
if (!data) {
this.#initRejection?.(`Content Type structure manager could not create scaffold`);
this.#initResolver = undefined;
this.#initRejection = undefined;
return { error: repsonse.error };
}
@@ -244,6 +254,8 @@ export class UmbContentTypeStructureManager<
// Make a entry in the repo manager:
this.#repoManager!.addEntry(data);
this.#initResolver?.(data);
this.#initResolver = undefined;
this.#initRejection = undefined;
return repsonse;
}
@@ -314,6 +326,10 @@ export class UmbContentTypeStructureManager<
return this.#contentTypes.getValue().find((y) => y.unique === this.#ownerContentTypeUnique);
}
getOwnerContentTypeName() {
return this.getOwnerContentType()?.name;
}
getOwnerContentTypeUnique() {
return this.#ownerContentTypeUnique;
}
@@ -977,16 +993,21 @@ export class UmbContentTypeStructureManager<
*/
/**
*
* Find merged containers that match the provided container ids.
* Notice if you can provide one or more ids matching the same container and it will still only return return the matching container once.
* @param containerIds - An array of container ids to find merged containers for.
* @param id
* Find a merged container that match the provided container id.
* @param {string} id - The id to find the merged container of.
* @returns {UmbPropertyTypeContainerMergedModel | undefined} - The merged containers that match the provided container ids.
*/
getMergedContainerById(id: string): UmbPropertyTypeContainerMergedModel | undefined {
return this.#mergedContainers.find((x) => x.ids.includes(id));
}
/**
* Find a merged container that match the provided merged-container key.
* @param {string} key - The key to find the merged container of.
* @returns {UmbPropertyTypeContainerMergedModel | undefined} - The merged containers that match the provided merged-container key.
*/
getMergedContainerByKey(key: string): UmbPropertyTypeContainerMergedModel | undefined {
return this.#mergedContainers.find((x) => x.key === key);
}
/**
*

View File

@@ -42,7 +42,7 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
@state()
private _groupId?: string;
@state()
@property({ type: Boolean, reflect: true, attribute: 'has-owner-container' })
private _hasOwnerContainer?: boolean;
// attrbute is used by Sorter Controller in parent scope.
@@ -273,12 +273,11 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
display: flex;
align-items: center;
justify-content: space-between;
cursor: grab;
padding: var(--uui-size-space-4) var(--uui-size-space-5);
}
:host([inherited]) div[slot='header'] {
cursor: default;
:host([has-owner-container]) div[slot='header'] {
cursor: grab;
}
div[slot='header'] > div {

View File

@@ -5,10 +5,10 @@ import type {
UmbPropertyTypeContainerModel,
} from '../../../types.js';
import { UmbContentTypeContainerStructureHelper } from '../../../structure/index.js';
import { UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT } from './content-type-design-editor.context-token.js';
import type { UmbContentTypeWorkspaceViewEditGroupElement } from './content-type-design-editor-group.element.js';
import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT } from './content-type-design-editor.context-token.js';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
@@ -19,13 +19,13 @@ import './content-type-design-editor-group.element.js';
const SORTER_CONFIG: UmbSorterConfig<UmbPropertyTypeContainerMergedModel, UmbContentTypeWorkspaceViewEditGroupElement> =
{
getUniqueOfElement: (element) => element.group?.key,
getUniqueOfModel: (modelEntry) => modelEntry.key,
getUniqueOfElement: (element) => element.group?.ownerId ?? element.group?.ids[0],
getUniqueOfModel: (modelEntry) => modelEntry.ownerId ?? modelEntry.ids[0],
// TODO: Make specific to the current owner document. [NL]
identifier: 'content-type-container-sorter',
itemSelector: 'umb-content-type-design-editor-group',
handleSelector: '.drag-handle',
disabledItemSelector: '[inherited]', // Inherited attribute is set by the umb-content-type-design-editor-group.
disabledItemSelector: ':not([has-owner-container])', // Inherited attribute is set by the umb-content-type-design-editor-group.
containerSelector: '.container-list',
};
@@ -38,18 +38,34 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
onChange: ({ model }) => {
this._groups = model;
},
onContainerChange: ({ item }) => {
if (this.#containerId === undefined) {
throw new Error('ContainerId is not set');
}
if (item.ownerId === undefined) {
// This may be possible later, but for now this is not possible. [NL]
throw new Error(
'OwnerId is not set for the given container, we cannot move containers that are not owned by the current Document.',
);
}
this.#groupStructureHelper.partialUpdateContainer(item.ownerId, {
parent: this.#containerId ? { id: this.#containerId } : null,
});
},
onEnd: ({ item }) => {
/*if (this._inherited === undefined) {
throw new Error('OwnerTabId is not set, we have not made a local duplicated of this container.');
return;
}*/
if (item.ownerId === undefined) {
// This may be possible later, but for now this is not possible. [NL]
throw new Error(
'OwnerId is not set for the given container, we cannot move containers that are not owned by the current Document.',
);
}
/**
* Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder.
* If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update
* the overlap if true, which may cause another overlap, so we loop through them till no more overlaps...
*/
const model = this._groups;
const newIndex = model.findIndex((entry) => entry.key === item.key);
const newIndex = model.findIndex((entry) => entry.ownerId === item.ownerId);
// Doesn't exist in model
if (newIndex === -1) return;
@@ -87,20 +103,22 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
this.#groupStructureHelper.partialUpdateContainer(entry.ownerId, {
sortOrder: ++prevSortOrder,
});
i++;
}
i++;
}
},
onRequestDrop: async ({ unique }) => {
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
const context = this.#contentTypeWorkspaceContext;
if (!context) {
throw new Error('Could not get Workspace Context');
}
return context.structure.getMergedContainerById(unique) as UmbPropertyTypeContainerMergedModel | undefined;
const result = context.structure.getMergedContainerById(unique) as
| UmbPropertyTypeContainerMergedModel
| undefined;
return result;
},
requestExternalRemove: async ({ item }) => {
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
const context = this.#contentTypeWorkspaceContext;
if (!context) {
throw new Error('Could not get Workspace Context');
}
@@ -110,7 +128,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
);
},
requestExternalInsert: async ({ item }) => {
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
const context = this.#contentTypeWorkspaceContext;
if (!context) {
throw new Error('Could not get Workspace Context');
}
@@ -165,11 +183,13 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
private _editContentTypePath?: string;
#groupStructureHelper = new UmbContentTypeContainerStructureHelper<UmbContentTypeModel>(this);
#contentTypeWorkspaceContext?: typeof UMB_CONTENT_TYPE_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (context) => {
this.#contentTypeWorkspaceContext = context;
this.#groupStructureHelper.setStructureManager(context?.structure);
const entityType = context?.getEntityType();
@@ -190,11 +210,6 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
context?.isSorting,
(isSorting) => {
this._sortModeActive = isSorting;
if (isSorting) {
this.#sorter.enable();
} else {
this.#sorter.disable();
}
},
'_observeIsSorting',
);
@@ -294,10 +309,20 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
align-content: start;
}
/* Ensure the container-list has some height when its empty so groups can be dropped into it.*/
.container-list {
margin-top: calc(var(--uui-size-layout-1) * -1);
padding-top: var(--uui-size-layout-1);
}
.container-list #convert-to-tab {
margin-bottom: var(--uui-size-layout-1);
display: flex;
}
.container-list[sort-mode-active] {
min-height: 100px;
}
`,
];
}

View File

@@ -752,7 +752,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
transition: opacity 100ms;
}
uui-tab:not(:hover, :focus) .trash {
uui-tab:not(:hover, :focus, :focus-within) .trash {
opacity: 0;
transition: opacity 100ms;
}

View File

@@ -10,6 +10,8 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
@query('#notifications')
private _notificationsElement?: HTMLElement;
@query('#sr-live') private _srLive?: HTMLDivElement;
@state()
private _notifications?: UmbNotificationHandler[];
@@ -29,7 +31,6 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
this.observe(this._notificationContext.notifications, (notifications) => {
this._notifications = notifications;
// Close and instantly open the popover again to make sure it stays on top of all other content on the page
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -39,9 +40,33 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._notificationsElement?.showPopover?.(); // To prevent issues in FireFox I added `?.` before `()` [NL]
//Announce the newest notification
this._announceNewest(this._notifications);
});
}
private _getNotificationText(notificatonData: UmbNotificationHandler): string {
//Trick to be able to access to the data(notification messagge) inside a private propertity
const data = (notificatonData as any)._data ?? {};
const notificationText = data.message ?? '';
return notificationText;
}
private _announce(message: string) {
if (!this._srLive) return;
this._srLive.textContent = 'u00A0'; //to avoid same text suppression
setTimeout(() => {
this._srLive!.textContent = message || '';
}, 0);
}
private _announceNewest(list?: UmbNotificationHandler[]) {
const newest = list?.[list.length - 1];
if (!newest) return;
this._announce(this._getNotificationText(newest));
}
override render() {
return html`
<uui-toast-notification-container bottom-up id="notifications" popover="manual">
@@ -49,10 +74,11 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
? repeat(
this._notifications,
(notification: UmbNotificationHandler) => notification.key,
(notification) => html`${notification.element}`,
(notification) => html`${notification.element} `,
)
: ''}
</uui-toast-notification-container>
<div id="sr-live" aria-live="assertive" aria-role="true"></div>
`;
}
@@ -66,7 +92,6 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
bottom: 45px;
height: auto;
padding: var(--uui-size-layout-1);
position: fixed;
width: 100vw;
background: 0;
@@ -74,6 +99,18 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
border: 0;
margin: 0;
}
#sr-live {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
clip: rect(0 0 0 0);
clip-path: inset(50%);
border: 0;
}
`,
];
}

View File

@@ -103,8 +103,12 @@ export class UmbModalRouteRegistrationController<
this.#init = this.consumeContext(UMB_ROUTE_CONTEXT, (_routeContext) => {
this.#routeContext = _routeContext;
this.#registerModal().catch(() => undefined);
}).asPromise({ preventTimeout: true });
if (this.#routeContext) {
this.#registerModal().catch(() => undefined);
}
})
.asPromise({ preventTimeout: true })
.catch(() => undefined);
}
/**
@@ -176,7 +180,7 @@ export class UmbModalRouteRegistrationController<
if (oldValue === value) return;
this.#uniquePaths.set(identifier, value);
this.#registerModal().catch(() => undefined);
this.#registerModal();
}
getUniquePathValue(identifier: string): string | undefined {
return this.#uniquePaths.get(identifier);
@@ -234,7 +238,7 @@ export class UmbModalRouteRegistrationController<
override hostConnected() {
super.hostConnected();
if (!this.#modalRegistrationContext) {
this.#registerModal().catch(() => undefined);
this.#registerModal();
}
}
override hostDisconnected(): void {

View File

@@ -238,7 +238,6 @@ export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> =
Partial<Pick<INTERNAL_UmbSorterConfig<T, ElementType>, 'ignorerSelector' | 'containerSelector' | 'identifier'>>;
/**
* @class UmbSorterController
* @implements {UmbControllerInterface}
* @description This controller can make user able to sort items.