Skip to content

Commit

Permalink
Fix: Improve sorter placement algorithm (#18021)
Browse files Browse the repository at this point in the history
* improve sorting algorithm

* fix block type input

* make confirm modal localizable

* rename method

* clean up

* clean up

* improve code

* Fix creating Block Types in Groups

* remove #moveData

* lint fixes

* remove unused

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
  • Loading branch information
nielslyngsoe and madsrasmussen authored Jan 20, 2025
1 parent 4353027 commit 6c1c851
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 197 deletions.
4 changes: 2 additions & 2 deletions src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2484,8 +2484,8 @@ export default {
confirmDeleteBlockTypeMessage: 'Are you sure you want to delete the block configuration <strong>%0%</strong>?',
confirmDeleteBlockTypeNotice:
'The content of this block will still be present, editing of this content\n will no longer be available and will be shown as unsupported content.\n ',
confirmDeleteBlockGroupMessage:
'Are you sure you want to delete group <strong>%0%</strong> and all the Block configurations of this?',
confirmDeleteBlockGroupTitle: 'Delete group?',
confirmDeleteBlockGroupMessage: 'Are you sure you want to delete group <strong>%0%</strong>?',
confirmDeleteBlockGroupNotice:
'The content of these Blocks will still be present, editing of this content\n will no longer be available and will be shown as unsupported content.\n ',
blockConfigurationOverlayTitle: "Configuration of '%0%'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, customElement, state, repeat, css, property, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import '../block-grid-entry/index.js';
import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter';
import {
UmbSorterController,
type UmbSorterConfig,
type UmbSorterResolvePlacementArgs,
} from '@umbraco-cms/backoffice/sorter';
import {
UmbFormControlMixin,
UmbFormControlValidator,
Expand All @@ -23,7 +27,9 @@ import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models';
* @param args
* @returns { null | true }
*/
function resolvePlacementAsGrid(args: resolvePlacementArgs<UmbBlockGridLayoutModel, UmbBlockGridEntryElement>) {
function resolvePlacementAsBlockGrid(
args: UmbSorterResolvePlacementArgs<UmbBlockGridLayoutModel, UmbBlockGridEntryElement>,
) {
// If this has areas, we do not want to move, unless we are at the edge
if (args.relatedModel.areas?.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) {
return null;
Expand Down Expand Up @@ -80,9 +86,16 @@ function resolvePlacementAsGrid(args: resolvePlacementArgs<UmbBlockGridLayoutMod
const relatedStartCol = Math.round(
getInterpolatedIndexOfPositionInWeightMap(relatedStartX, approvedContainerGridColumns),
);

// If the found related element does not have enough room after which for the current element, then we go vertical mode:
return relatedStartCol + (args.horizontalPlaceAfter ? foundElColumns : 0) + currentElementColumns > gridColumnNumber;
const verticalDirection = relatedStartCol + foundElColumns + currentElementColumns > gridColumnNumber;
return verticalDirection;
/*
let placeAfter = args.horizontalPlaceAfter;
return {
verticalDirection,
placeAfter,
};*/
}

// --------------------------
Expand All @@ -96,7 +109,7 @@ const SORTER_CONFIG: UmbSorterConfig<UmbBlockGridLayoutModel, UmbBlockGridEntryE
getUniqueOfModel: (modelEntry) => {
return modelEntry.contentKey;
},
resolvePlacement: resolvePlacementAsGrid,
resolvePlacement: resolvePlacementAsBlockGrid,
identifier: 'block-grid-editor',
itemSelector: 'umb-block-grid-entry',
containerSelector: '.umb-block-grid__layout-container',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';
import '../../../block-type/components/input-block-type/index.js';
import {
type UmbPropertyEditorUiElement,
UmbPropertyValueChangeEvent,
type UmbPropertyEditorConfigCollection,
import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';
import type {
UmbPropertyEditorUiElement,
UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import {
html,
Expand All @@ -30,6 +28,8 @@ import {
} from '@umbraco-cms/backoffice/property';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';

interface MappedGroupWithBlockTypes extends UmbBlockGridTypeGroupType {
blocks: Array<UmbBlockTypeWithGroupKey>;
Expand All @@ -43,7 +43,6 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
#moveData?: Array<UmbBlockTypeWithGroupKey>;
#sorter = new UmbSorterController<MappedGroupWithBlockTypes, HTMLElement>(this, {
getUniqueOfElement: (element) => element.getAttribute('data-umb-group-key'),
getUniqueOfModel: (modelEntry) => modelEntry.key!,
Expand Down Expand Up @@ -104,8 +103,14 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement

this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
this.#datasetContext = context;
//this.#observeBlocks();
this.#observeBlockGroups();
this.observe(
await this.#datasetContext.propertyValueByAlias('blockGroups'),
(value) => {
this.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];
this.#mapValuesToBlockGroups();
},
'_observeBlockGroups',
);
});

this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(
Expand All @@ -119,24 +124,6 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
});
}

async #observeBlockGroups() {
if (!this.#datasetContext) return;
this.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {
this.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];
this.#mapValuesToBlockGroups();
});
}
// TODO: No need for this, we just got the value via the value property.. [NL]
/*
async #observeBlocks() {
if (!this.#datasetContext) return;
this.observe(await this.#datasetContext.propertyValueByAlias('blocks'), (value) => {
this.value = (value as Array<UmbBlockTypeWithGroupKey>) ?? [];
this.#mapValuesToBlockGroups();
});
}
*/

#mapValuesToBlockGroups() {
if (!this.#blockGroups) return;
// Map blocks that are not in any group, or in a group that does not exist
Expand All @@ -152,63 +139,60 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
this.#sorter.setModel(this._groupsWithBlockTypes);
}

#onDelete(e: CustomEvent, groupKey?: string) {
const updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));
const filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);
this.value = [...filteredValues, ...updatedValues];
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}

async #onChange(e: CustomEvent) {
async #onChange(e: Event, groupKey?: string) {
e.stopPropagation();
const element = e.target as UmbInputBlockTypeElement;
const value = element.value;

if (!e.detail?.moveComplete) {
// Container change, store data of the new group...
const newGroupKey = element.getAttribute('data-umb-group-key');
const movedItem = e.detail?.item as UmbBlockTypeWithGroupKey;
// Check if item moved back to original group...
if (movedItem.groupKey === newGroupKey) {
this.#moveData = undefined;
} else {
this.#moveData = value.map((block) => ({ ...block, groupKey: newGroupKey }));
}
} else if (e.detail?.moveComplete) {
// Move complete, get the blocks that were in an untouched group
const blocks = this.#value
.filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))
.filter(
(block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),
);

this.value = this.#moveData ? [...blocks, ...value, ...this.#moveData] : [...blocks, ...value];
this.dispatchEvent(new UmbPropertyValueChangeEvent());
this.#moveData = undefined;
const value = element.value.map((x) => ({ ...x, groupKey }));

if (groupKey) {
// Update the specific group:
this._groupsWithBlockTypes = this._groupsWithBlockTypes.map((group) => {
if (group.key === groupKey) {
return { ...group, blocks: value };
}
return group;
});
} else {
// Update the not grouped blocks:
this._notGroupedBlockTypes = value;
}

this.#updateValue();
}

#updateValue() {
this.value = [...this._notGroupedBlockTypes, ...this._groupsWithBlockTypes.flatMap((group) => group.blocks)];
this.dispatchEvent(new UmbChangeEvent());
}

#updateBlockGroupsValue(groups: Array<UmbBlockGridTypeGroupType>) {
this.#datasetContext?.setPropertyValue('blockGroups', groups);
}

#onCreate(e: CustomEvent, groupKey?: string) {
const selectedElementType = e.detail.contentElementTypeKey;
if (selectedElementType) {
this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? null));
this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? 'null'));
}
}

// TODO: Implement confirm dialog [NL]
#deleteGroup(groupKey: string) {
// TODO: make one method for updating the blockGroupsDataSetValue: [NL]
// This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.
this.#datasetContext?.setPropertyValue(
'blockGroups',
this.#blockGroups?.filter((group) => group.key !== groupKey),
);

async #deleteGroup(groupKey: string) {
const groupName = this.#blockGroups?.find((group) => group.key === groupKey)?.name ?? '';
await umbConfirmModal(this, {
headline: '#blockEditor_confirmDeleteBlockGroupTitle',
content: this.localize.term('#blockEditor_confirmDeleteBlockGroupMessage', [groupName]),
color: 'danger',
confirmLabel: '#general_delete',
});
// If a group is deleted, Move the blocks to no group:
this.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));
if (this.#blockGroups) {
this.#updateBlockGroupsValue(this.#blockGroups.filter((group) => group.key !== groupKey));
}
}

#changeGroupName(e: UUIInputEvent, groupKey: string) {
#onGroupNameChange(e: UUIInputEvent, groupKey: string) {
const groupName = e.target.value as string;
// TODO: make one method for updating the blockGroupsDataSetValue: [NL]
this.#datasetContext?.setPropertyValue(
Expand All @@ -224,9 +208,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
.propertyAlias=${this._alias}
.value=${this._notGroupedBlockTypes}
.workspacePath=${this._workspacePath}
@change=${this.#onChange}
@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}
@delete=${(e: CustomEvent) => this.#onDelete(e, undefined)}></umb-input-block-type>`
@change=${(e: Event) => this.#onChange(e, undefined)}
@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}></umb-input-block-type>`
: ''}
${repeat(
this._groupsWithBlockTypes,
Expand All @@ -239,9 +222,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
.propertyAlias=${this._alias + '_' + group.key}
.value=${group.blocks}
.workspacePath=${this._workspacePath}
@change=${this.#onChange}
@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}
@delete=${(e: CustomEvent) => this.#onDelete(e, group.key)}></umb-input-block-type>
@change=${(e: Event) => this.#onChange(e, group.key)}
@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}></umb-input-block-type>
</div>`,
)}
</div>`;
Expand All @@ -253,7 +235,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
auto-width
label="Group"
.value=${groupName ?? ''}
@change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>
@change=${(e: UUIInputEvent) => this.#onGroupNameChange(e, groupKey)}>
<uui-button compact slot="append" label="delete" @click=${() => this.#deleteGroup(groupKey)}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { css, html, customElement, property, state, repeat } from '@umbraco-cms/
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import {
UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT,
UMB_DOCUMENT_TYPE_PICKER_MODAL,
type UmbDocumentTypePickerModalData,
type UmbDocumentTypePickerModalValue,
} from '@umbraco-cms/backoffice/document-type';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';

import '../block-type-card/index.js';
Expand All @@ -27,32 +29,40 @@ export class UmbInputBlockTypeElement<
itemSelector: 'umb-block-type-card',
identifier: 'umb-block-type-sorter',
containerSelector: '#blocks',
onChange: ({ model }) => {
this._items = model;
resolvePlacement: UmbSorterResolvePlacementAsGrid,
onContainerChange: ({ item, model }) => {
this.dispatchEvent(new CustomEvent('container-change', { detail: { item, model } }));
},
onContainerChange: ({ model, item }) => {
this._items = model;
this.dispatchEvent(new CustomEvent('change', { detail: { item } }));
onChange: ({ model }) => {
this._value = model;
this.dispatchEvent(new UmbChangeEvent());
},
onEnd: () => {
/*onEnd: () => {
// TODO: Investigate if onEnd is called when a container move has been performed, if not then I would say it should be. [NL]
this.dispatchEvent(new CustomEvent('change', { detail: { moveComplete: true } }));
},
this.dispatchEvent(new UmbChangeEvent());
},*/
});
#elementPickerModal: UmbModalRouteRegistrationController<
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalValue
>;

@property({ type: Array, attribute: false })
public set value(items) {
this._items = items ?? [];
this.#sorter.setModel(this._items);
this._value = items ?? [];
// Make sure the block types are unique on contentTypeElementKey:
this._value = this._value.filter(
(value, index, self) => self.findIndex((x) => x.contentElementTypeKey === value.contentElementTypeKey) === index,
);
this.#sorter.setModel(this._value);
}
public get value() {
return this._items;
return this._value;
}

/** @deprecated will be removed in v17 */
@property({ type: String })
public set propertyAlias(value: string | undefined) {
//this.#elementPickerModal.setUniquePathValue('propertyAlias', value);
this.#elementPickerModal.setUniquePathValue('propertyAlias', value);
}
public get propertyAlias(): string | undefined {
return undefined;
Expand All @@ -65,7 +75,7 @@ export class UmbInputBlockTypeElement<
private _pickerPath?: string;

@state()
private _items: Array<BlockType> = [];
private _value: Array<BlockType> = [];

// TODO: Seems no need to have these initially, then can be retrieved inside the `create` method. [NL]
#datasetContext?: UmbPropertyDatasetContext;
Expand All @@ -84,7 +94,8 @@ export class UmbInputBlockTypeElement<
);
});

new UmbModalRouteRegistrationController(this, UMB_DOCUMENT_TYPE_PICKER_MODAL)
this.#elementPickerModal = new UmbModalRouteRegistrationController(this, UMB_DOCUMENT_TYPE_PICKER_MODAL)
.addUniquePaths(['propertyAlias'])
.onSetup(() => {
return {
data: {
Expand Down Expand Up @@ -123,8 +134,8 @@ export class UmbInputBlockTypeElement<
}

deleteItem(contentElementTypeKey: string) {
this.value = this.value.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.dispatchEvent(new UmbDeleteEvent());
this._value = this.value.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.dispatchEvent(new UmbChangeEvent());
}

async #onRequestDelete(item: BlockType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { UmbRefItemElement } from '@umbraco-cms/backoffice/components';
import type {
UmbEntityCreateOptionActionListModalData,
UmbEntityCreateOptionActionListModalValue,
} from './entity-create-option-action-list-modal.token.js';
import { UmbRefItemElement } from '@umbraco-cms/backoffice/components';
import type { ManifestEntityCreateOptionAction } from '@umbraco-cms/backoffice/entity-create-option-action';
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
Expand Down
Loading

0 comments on commit 6c1c851

Please sign in to comment.