From d65cf550bcd46831eb486f2cd102fba41cfcb39f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Tue, 18 Apr 2023 12:55:53 +0200
Subject: [PATCH] initial sorting implementation
---
.../PropertyTypeResponseModelBaseModel.ts | 21 +++----
.../filter-frozen-array.function.ts | 13 ++++
.../libs/observable-api/index.ts | 1 +
.../libs/sorter/sorter.controller.ts | 55 +++++++++++-----
...-workspace-view-edit-properties.element.ts | 63 ++++++++++++-------
...pe-workspace-view-edit-property.element.ts | 6 +-
...nt-type-workspace-view-edit-tab.element.ts | 2 +
...rkspace-property-structure-helper.class.ts | 35 ++++++++---
.../workspace-structure-manager.class.ts | 24 +++++++
9 files changed, 162 insertions(+), 58 deletions(-)
create mode 100644 src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts
diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts
index ad3a59bbbf..72adb84e18 100644
--- a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts
+++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts
@@ -6,15 +6,14 @@ import type { PropertyTypeAppearanceModel } from './PropertyTypeAppearanceModel'
import type { PropertyTypeValidationModel } from './PropertyTypeValidationModel';
export type PropertyTypeResponseModelBaseModel = {
- id?: string;
- containerId?: string | null;
- alias?: string;
- name?: string;
- description?: string | null;
- dataTypeId?: string;
- variesByCulture?: boolean;
- variesBySegment?: boolean;
- validation?: PropertyTypeValidationModel;
- appearance?: PropertyTypeAppearanceModel;
+ id?: string;
+ containerId?: string | null;
+ alias?: string;
+ name?: string;
+ description?: string | null;
+ dataTypeId?: string;
+ variesByCulture?: boolean;
+ variesBySegment?: boolean;
+ validation?: PropertyTypeValidationModel;
+ appearance?: PropertyTypeAppearanceModel;
};
-
diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts
new file mode 100644
index 0000000000..7d3e5a2e0c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts
@@ -0,0 +1,13 @@
+/**
+ * @export
+ * @method filterFrozenArray
+ * @param {Array} data - RxJS Subject to use for this Observable.
+ * @param {(entry: T) => boolean} filterMethod - Method to filter the array.
+ * @description - Creates a RxJS Observable from RxJS Subject.
+ * @example Example remove an entry of a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.
+ * const newDataSet = filterFrozenArray(mySubject.getValue(), x => x.id !== "myKey");
+ * mySubject.next(newDataSet);
+ */
+export function filterFrozenArray(data: T[], filterMethod: (entry: T) => boolean): T[] {
+ return [...data].filter((x) => filterMethod(x));
+}
diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts
index c18d4495b7..e0edfe4be9 100644
--- a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts
+++ b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts
@@ -10,5 +10,6 @@ export * from './array-state';
export * from './object-state';
export * from './create-observable-part.function';
export * from './append-to-frozen-array.function';
+export * from './filter-frozen-array.function';
export * from './partial-update-frozen-array.function';
export * from './mapping-function';
diff --git a/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts
index 13014e52bb..554b9e5a4e 100644
--- a/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts
+++ b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts
@@ -96,6 +96,8 @@ type INTERNAL_UmbSorterConfig = {
placeholderIsInThisRow: boolean;
horizontalPlaceAfter: boolean;
}) => void;
+ performItemInsert?: (argument: { item: T; newIndex: number }) => Promise | boolean;
+ performItemRemove?: (argument: { item: T }) => Promise | boolean;
};
// External type with some properties optional, as they have defaults:
@@ -191,7 +193,10 @@ export class UmbSorterController implements UmbControllerInterface {
// TODO: Clean up??
this.#observer.disconnect();
- this.#observer.observe(this.#containerElement, { childList: true, subtree: false });
+ this.#observer.observe(this.#containerElement.shadowRoot ?? this.#containerElement, {
+ childList: true,
+ subtree: false,
+ });
}
hostDisconnected() {
// TODO: Clean up??
@@ -227,7 +232,7 @@ export class UmbSorterController implements UmbControllerInterface {
}
if (!this.#scrollElement) {
- this.#scrollElement = getParentScrollElement(this.#host, true);
+ this.#scrollElement = getParentScrollElement(this.#containerElement, true);
}
const element = (event.target as HTMLElement).closest(this.#config.itemSelector);
@@ -270,7 +275,7 @@ export class UmbSorterController implements UmbControllerInterface {
// We must wait one frame before changing the look of the block.
this.#rqaId = requestAnimationFrame(() => {
- // It should be okay to use the same rqafId, as the move does not or is okay not to happen on first frame/drag-move.
+ // It should be okay to use the same rqaId, as the move does not or is okay not to happen on first frame/drag-move.
this.#rqaId = undefined;
if (this.#currentElement) {
this.#currentElement.style.transform = '';
@@ -279,7 +284,7 @@ export class UmbSorterController implements UmbControllerInterface {
});
};
- handleDragEnd = () => {
+ handleDragEnd = async () => {
if (!this.#currentElement || !this.#currentItem) {
return;
}
@@ -292,7 +297,7 @@ export class UmbSorterController implements UmbControllerInterface {
this.stopAutoScroll();
this.removeAllowIndication();
- if (this.#currentContainerVM.sync(this.#currentElement, this) === false) {
+ if ((await this.#currentContainerVM.sync(this.#currentElement, this)) === false) {
// Sync could not succeed, might be because item is not allowed here.
this.#currentContainerVM = this;
@@ -419,7 +424,11 @@ export class UmbSorterController implements UmbControllerInterface {
}
// We want to retrieve the children of the container, every time to ensure we got the right order and index
- const orderedContainerElements = Array.from(this.#currentContainerElement.children);
+ const orderedContainerElements = Array.from(
+ this.#currentContainerElement.shadowRoot
+ ? this.#currentContainerElement.shadowRoot.children
+ : this.#currentContainerElement.children
+ );
const currentContainerRect = this.#currentContainerElement.getBoundingClientRect();
@@ -573,18 +582,20 @@ export class UmbSorterController implements UmbControllerInterface {
};
move(orderedContainerElements: Array, newElIndex: number) {
- if (!this.#currentElement || !this.#currentItem) return;
+ if (!this.#currentElement || !this.#currentItem || !this.#currentContainerElement) return;
newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex;
+ const containerElement = this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement;
+
const placeBeforeElement = orderedContainerElements[newElIndex];
if (placeBeforeElement) {
// We do not need to move this, if the element to be placed before is it self.
if (placeBeforeElement !== this.#currentElement) {
- this.#currentContainerElement.insertBefore(this.#currentElement, placeBeforeElement);
+ containerElement.insertBefore(this.#currentElement, placeBeforeElement);
}
} else {
- this.#currentContainerElement.appendChild(this.#currentElement);
+ containerElement.appendChild(this.#currentElement);
}
if (this.#config.onChange) {
@@ -605,13 +616,18 @@ export class UmbSorterController implements UmbControllerInterface {
return this.#model.find((entry: T) => this.#config.compareElementToModel(element, entry));
}
- public removeItem(item: T) {
+ public async removeItem(item: T) {
if (!item) {
return null;
}
- const oldIndex = this.#model.indexOf(item);
- if (oldIndex !== -1) {
- return this.#model.splice(oldIndex, 1)[0];
+
+ if (this.#config.performItemRemove) {
+ return await this.#config.performItemRemove({ item });
+ } else {
+ const oldIndex = this.#model.indexOf(item);
+ if (oldIndex !== -1) {
+ return this.#model.splice(oldIndex, 1)[0];
+ }
}
return null;
}
@@ -620,7 +636,7 @@ export class UmbSorterController implements UmbControllerInterface {
return this.#model.filter((x) => x !== item).length > 0;
}
- public sync(element: HTMLElement, fromVm: UmbSorterController) {
+ public async sync(element: HTMLElement, fromVm: UmbSorterController) {
const movingItem = fromVm.getItemOfElement(element);
if (!movingItem) {
console.error('Could not find item of sync item');
@@ -651,11 +667,18 @@ export class UmbSorterController implements UmbControllerInterface {
let newIndex = this.#model.length;
if (nextEl) {
// We had a reference element, we want to get the index of it.
- // This is problem if a item is being moved forward?
+ // This is might a problem if a item is being moved forward? (was also like this in the AngularJS version...)
newIndex = this.#model.findIndex((entry) => this.#config.compareElementToModel(nextEl! as HTMLElement, entry));
}
- this.#model.splice(newIndex, 0, movingItem);
+ if (this.#config.performItemInsert) {
+ const result = await this.#config.performItemInsert({ item: movingItem, newIndex });
+ if (result === false) {
+ return false;
+ }
+ } else {
+ this.#model.splice(newIndex, 0, movingItem);
+ }
const eventData = { item: movingItem, fromController: fromVm, toController: this };
if (fromVm !== this) {
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts
index 120de1b390..99818e9e19 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts
@@ -2,18 +2,53 @@ import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
+import { UmbSorterController, UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
+import { ifDefined } from 'lit/directives/if-defined.js';
import { UmbWorkspacePropertyStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-property-structure-helper.class';
import { PropertyContainerTypes } from '../../../../../shared/components/workspace/workspace-context/workspace-structure-manager.class';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_MODAL_CONTEXT_TOKEN, UMB_PROPERTY_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal';
import './document-type-workspace-view-edit-property.element';
-import { UmbSorterController } from '@umbraco-cms/sorter';
+const SORTER_CONFIG: UmbSorterConfig = {
+ compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeResponseModel) => {
+ return element.getAttribute('data-umb-property-id') === model.id;
+ },
+ querySelectModelToElement: (container: HTMLElement, modelEntry: DocumentTypePropertyTypeResponseModel) => {
+ return container.querySelector('data-umb-property-id[' + modelEntry.id + ']');
+ },
+ identifier: 'content-type-property-sorter',
+ itemSelector: '[data-umb-property-id][data-property-of-owner-document]',
+};
@customElement('umb-document-type-workspace-view-edit-properties')
export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitElement {
+ #propertySorter = new UmbSorterController(this, {
+ ...SORTER_CONFIG,
+ performItemInsert: (args) => {
+ let sortOrder = 0;
+ if (this._propertyStructure.length > 0) {
+ if (args.newIndex === 0) {
+ // TODO: Remove as any when sortOrder is added to the model:
+ sortOrder = ((this._propertyStructure[0] as any).sortOrder ?? 0) - 1;
+ } else {
+ sortOrder =
+ ((this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)] as any).sortOrder ??
+ 0) + 1;
+ }
+ }
+ console.log('insert', args.item.id, sortOrder);
+ return this._propertyStructureHelper.insertProperty(args.item, sortOrder);
+ },
+ performItemRemove: (args) => {
+ console.log('remove', args.item.id);
+ return this._propertyStructureHelper.removeProperty(args.item.id!);
+ },
+ });
+
private _containerId: string | undefined;
+ @property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | undefined {
return this._containerId;
}
@@ -47,8 +82,6 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
#modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE;
- #propertySorter = new UmbSorterController(this, sorterConfig);
-
constructor() {
super();
@@ -79,6 +112,11 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
(property) => property.alias,
(property) =>
html` {
this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail);
@@ -99,25 +137,6 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
border-bottom: 0;
}
- .property {
- display: grid;
- grid-template-columns: 200px auto;
- column-gap: var(--uui-size-layout-2);
- border-bottom: 1px solid var(--uui-color-divider);
- padding: var(--uui-size-layout-1) 0;
- container-type: inline-size;
- }
-
- .property > div {
- grid-column: span 2;
- }
-
- @container (width > 600px) {
- .property:not([orientation='vertical']) > div {
- grid-column: span 1;
- }
- }
-
#add {
width: 100%;
}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts
index 6b0b0d3ea8..9e08199e49 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts
@@ -50,7 +50,7 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
}}>
-
+
`
: '';
}
@@ -95,6 +95,10 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
height: min-content;
z-index: 2;
}
+
+ #editor {
+ background-color: var(--uui-color-background);
+ }
`,
];
}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts
index 6b3c935ea8..8bf2ac1618 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts
@@ -87,6 +87,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
? html`
@@ -97,6 +98,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
(group) => group.name,
(group) => html`
`
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts
index 8a3f2e2844..eae3858873 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts
@@ -1,6 +1,9 @@
import { UmbDocumentWorkspaceContext } from '../../../../documents/documents/workspace/document-workspace.context';
import { PropertyContainerTypes } from './workspace-structure-manager.class';
-import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
+import {
+ DocumentTypePropertyTypeResponseModel,
+ PropertyTypeResponseModelBaseModel,
+} from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController, UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api';
import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
@@ -20,6 +23,8 @@ export class UmbWorkspacePropertyStructureHelper {
constructor(host: UmbControllerHostElement) {
this.#host = host;
+ // TODO: Remove as any when sortOrder is implemented:
+ this.#propertyStructure.sortBy((a, b) => ((a as any).sortOrder ?? 0) - ((b as any).sortOrder ?? 0));
this.#init = new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => {
this.#workspaceContext = context as UmbDocumentWorkspaceContext;
this._observeGroupContainers();
@@ -86,11 +91,6 @@ export class UmbWorkspacePropertyStructureHelper {
}
});
- if (_propertyStructure.length > 0) {
- // TODO: End-point: Missing sort order?
- //_propertyStructure = _propertyStructure.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
- }
-
// Fire update to subscribers:
this.#propertyStructure.next(_propertyStructure);
},
@@ -101,11 +101,30 @@ export class UmbWorkspacePropertyStructureHelper {
// TODO: consider moving this to another class, to separate 'viewer' from 'manipulator':
/** Manipulate methods: */
- async addProperty(ownerKey?: string, sortOrder?: number) {
+ async addProperty(ownerId?: string, sortOrder?: number) {
await this.#init;
if (!this.#workspaceContext) return;
- return await this.#workspaceContext.structure.createProperty(null, ownerKey, sortOrder);
+ return await this.#workspaceContext.structure.createProperty(null, ownerId, sortOrder);
+ }
+
+ async insertProperty(property: PropertyTypeResponseModelBaseModel, sortOrder = 0) {
+ await this.#init;
+ if (!this.#workspaceContext) return false;
+
+ const newProperty = { ...property, sortOrder };
+
+ // TODO: Remove as any when server model has gotten sortOrder:
+ await this.#workspaceContext.structure.insertProperty(null, newProperty);
+ return true;
+ }
+
+ async removeProperty(propertyId: string) {
+ await this.#init;
+ if (!this.#workspaceContext) return false;
+
+ await this.#workspaceContext.structure.removeProperty(null, propertyId);
+ return true;
}
// Takes optional arguments as this is easier for the implementation in the view:
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts
index fd6d6ad19b..7d8d2f84d0 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts
@@ -13,6 +13,8 @@ import {
UmbObserverController,
MappingFunction,
partialUpdateFrozenArray,
+ appendToFrozenArray,
+ filterFrozenArray,
} from '@umbraco-cms/backoffice/observable-api';
export type PropertyContainerTypes = 'Group' | 'Tab';
@@ -218,6 +220,28 @@ export class UmbWorkspacePropertyStructureManager x.id === documentTypeId)?.properties ?? [];
+
+ const properties = appendToFrozenArray(frozenProperties, property, (x) => x.id === property.id);
+
+ this.#documentTypes.updateOne(documentTypeId, { properties });
+ }
+
+ async removeProperty(documentTypeId: string | null, propertyId: string) {
+ await this.#init;
+ documentTypeId = documentTypeId ?? this.#rootDocumentTypeId!;
+
+ const frozenProperties = this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.properties ?? [];
+
+ const properties = filterFrozenArray(frozenProperties, (x) => x.id === propertyId);
+
+ this.#documentTypes.updateOne(documentTypeId, { properties });
+ }
+
async updateProperty(
documentTypeId: string | null,
propertyId: string,