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,