Content Type Designer: Fix moving a group to an inherited tab (#20138)

move group to inherited tab
This commit is contained in:
Niels Lyngsø
2025-10-08 18:49:48 +02:00
committed by GitHub
parent 7ecc6ece7e
commit 9a18dd547d
7 changed files with 80 additions and 36 deletions

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;
}
@@ -997,16 +1009,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

@@ -753,7 +753,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

@@ -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

@@ -247,7 +247,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.