Merge pull request #1596 from umbraco/bugfix/block-grid-areas

Create Blocks in a Area
This commit is contained in:
Niels Lyngsø
2024-04-22 09:51:31 +02:00
committed by GitHub
12 changed files with 176 additions and 121 deletions

View File

@@ -16,7 +16,7 @@ export const manifests: Array<ManifestTypes> = [
{
type: 'workspaceAction',
kind: 'default',
name: 'Example Count Incerementor Workspace Action',
name: 'Example Count Incrementor Workspace Action',
alias: 'example.workspaceAction.incrementor',
weight: 1000,
api: () => import('./incrementor-workspace-action.js'),

View File

@@ -45,6 +45,9 @@ export class UmbBlockGridAreaConfigEntryContext
getRowSpan() {
return this.#area.getValue()?.rowSpan;
}
getAlias() {
return this.#area.getValue()?.alias;
}
public getRelevantColumnSpanOptions() {
const layoutColumns = this.#entriesContext?.getLayoutColumns();
if (!layoutColumns) return;
@@ -88,7 +91,7 @@ export class UmbBlockGridAreaConfigEntryContext
async requestDelete() {
await umbConfirmModal(this, {
headline: `Delete ${this.alias}`,
headline: `Delete ${this.getAlias()}`,
content: 'Are you sure you want to delete this Area?',
confirmLabel: 'Delete',
color: 'danger',

View File

@@ -19,6 +19,9 @@ export class UmbBlockGridAreaTypeWorkspaceEditorElement extends UmbLitElement {
this.consumeContext(UMB_BLOCK_GRID_AREA_TYPE_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance;
this.observe(this.#workspaceContext.name, (name) => {
this._name = name;
});
this.#workspaceContext?.createPropertyDatasetContext(this);
});
}
@@ -31,7 +34,7 @@ export class UmbBlockGridAreaTypeWorkspaceEditorElement extends UmbLitElement {
alias=${this.workspaceAlias}
headline=${this.localize.term('blockEditor_blockConfigurationOverlayTitle', [this._name])}>
</umb-workspace-editor>
`
`
: '';
}

View File

@@ -1,7 +1,12 @@
import type { UmbBlockGridTypeAreaType } from '../../../types.js';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UmbInvariantDatasetWorkspaceContext, UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import {
type UmbInvariantDatasetWorkspaceContext,
type UmbRoutableWorkspaceContext,
type UmbWorkspaceContext,
UmbWorkspaceRouteManager,
} from '@umbraco-cms/backoffice/workspace';
import {
UmbSubmittableWorkspaceContextBase,
UmbInvariantWorkspacePropertyDatasetContext,
@@ -13,11 +18,13 @@ import type { ManifestWorkspace, PropertyEditorSettingsProperty } from '@umbraco
export class UmbBlockGridAreaTypeWorkspaceContext
extends UmbSubmittableWorkspaceContextBase<UmbBlockGridTypeAreaType>
implements UmbInvariantDatasetWorkspaceContext
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext
{
// Just for context token safety:
public readonly IS_BLOCK_GRID_AREA_TYPE_WORKSPACE_CONTEXT = true;
readonly routes = new UmbWorkspaceRouteManager(this);
#entityType: string;
#data = new UmbObjectState<UmbBlockGridTypeAreaType | undefined>(undefined);
readonly data = this.#data.asObservable();
@@ -34,6 +41,20 @@ export class UmbBlockGridAreaTypeWorkspaceContext
this.#entityType = workspaceArgs.manifest.meta?.entityType;
}
set manifest(manifest: ManifestWorkspace) {
this.routes.setRoutes([
{
path: 'edit/:id',
component: () => import('./block-grid-area-type-workspace-editor.element.js'),
setup: (_component, info) => {
const id = info.match.params.id;
(_component as any).workspaceAlias = manifest.alias;
this.load(id);
},
},
]);
}
protected resetState(): void {
super.resetState();
this.#data.setValue(undefined);
@@ -50,6 +71,7 @@ export class UmbBlockGridAreaTypeWorkspaceContext
if (value) {
const blockTypeData = value.find((x: UmbBlockGridTypeAreaType) => x.key === unique);
if (blockTypeData) {
console.log(blockTypeData);
this.#data.setValue(blockTypeData);
return;
}
@@ -86,10 +108,10 @@ export class UmbBlockGridAreaTypeWorkspaceContext
}
getName() {
return 'block name content element type here...';
return this.#data.getValue()?.alias;
}
setName(name: string | undefined) {
alert('You cannot set a name of a block-type.');
throw new Error('You cannot set a name of a area-type.');
}
async propertyValueByAlias<ReturnType = unknown>(propertyAlias: keyof UmbBlockGridTypeAreaType) {

View File

@@ -1,78 +0,0 @@
import type { UmbBlockGridAreaTypeWorkspaceContext } from './block-grid-area-type-workspace.context.js';
import { UmbBlockGridAreaTypeWorkspaceEditorElement } from './block-grid-area-type-workspace-editor.element.js';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsApiInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-block-grid-area-type-workspace')
export class UmbBlockGridAreaTypeWorkspaceElement extends UmbLitElement {
//
#manifest?: ManifestWorkspace;
#workspaceContext?: UmbBlockGridAreaTypeWorkspaceContext;
#editorElement = () => {
const element = new UmbBlockGridAreaTypeWorkspaceEditorElement();
element.workspaceAlias = this.#manifest!.alias;
return element;
};
@state()
_routes: UmbRoute[] = [];
public set manifest(manifest: ManifestWorkspace) {
this.#manifest = manifest;
createExtensionApi(this, manifest, [{ manifest: manifest }]).then((context) => {
if (context) {
this.#gotWorkspaceContext(context);
}
});
}
#gotWorkspaceContext(context: UmbApi) {
this.#workspaceContext = context as UmbBlockGridAreaTypeWorkspaceContext;
this._routes = [
/*
{
path: 'create',
component: this.#editorElement,
setup: async (_component, info) => {
this.#workspaceContext!.create();
new UmbWorkspaceIsNewRedirectController(
this,
this.#workspaceContext!,
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},*/
{
path: 'edit/:id',
component: this.#editorElement,
setup: (_component, info) => {
const id = info.match.params.id;
this.#workspaceContext!.load(id);
},
},
];
// TODO: We need to recreate when ID changed?
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [this, this.#workspaceContext]);
}
render() {
return html`<umb-router-slot .routes="${this._routes}"></umb-router-slot>`;
}
}
export default UmbBlockGridAreaTypeWorkspaceElement;
declare global {
interface HTMLElementTagNameMap {
'umb-block-grid-area-type-workspace': UmbBlockGridAreaTypeWorkspaceElement;
}
}

View File

@@ -7,9 +7,9 @@ export const manifests: Array<ManifestTypes> = [
...workspaceViewManifests,
{
type: 'workspace',
kind: 'routable',
name: 'Block Grid Area Type Workspace',
alias: UMB_BLOCK_GRID_AREA_TYPE_WORKSPACE_ALIAS,
element: () => import('./block-grid-area-type-workspace.element.js'),
api: () => import('./block-grid-area-type-workspace.context.js'),
meta: {
entityType: 'block-grid-area-type',
@@ -17,6 +17,7 @@ export const manifests: Array<ManifestTypes> = [
},
{
type: 'workspaceAction',
kind: 'default',
alias: 'Umb.WorkspaceAction.BlockGridAreaType.Save',
name: 'Save Block Grid Area Type Workspace Action',
api: UmbSubmitWorkspaceAction,

View File

@@ -206,14 +206,14 @@ export class UmbBlockGridEntriesElement extends UmbLitElement {
look="placeholder"
href=${this.#context.getPathForClipboard(-1) ?? ''}>
<uui-icon name="icon-paste-in"></uui-icon>
</uui-button>`
</uui-button>`
: ''}
</uui-button-group>`
</uui-button-group>`
: html`
<uui-button-inline-create
href=${this.#context.getPathForCreateBlock(-1) ?? ''}
label=${this.localize.term('blockEditor_addBlock')}></uui-button-inline-create>
`}
`}
`;
}

View File

@@ -7,6 +7,7 @@ import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-ma
import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { pathFolderName } from '@umbraco-cms/backoffice/utils';
export class UmbBlockGridEntriesContext
extends UmbBlockEntriesContext<
@@ -27,12 +28,14 @@ export class UmbBlockGridEntriesContext
#areaType?: UmbBlockGridTypeAreaType;
//#parentUnique?: string;
#parentUnique?: string | null;
#areaKey?: string | null;
setParentUnique(contentUdi: string | null) {
this._workspaceModal.setUniquePathValue('parentUnique', contentUdi ?? 'null');
this.#catalogueModal.setUniquePathValue('parentUnique', contentUdi ?? 'null');
this.#parentUnique = contentUdi;
// Notice pathFolderName can be removed when we have switched to use a proper GUID/ID/KEY. [NL]
this._workspaceModal.setUniquePathValue('parentUnique', pathFolderName(contentUdi ?? 'null'));
this.#catalogueModal.setUniquePathValue('parentUnique', pathFolderName(contentUdi ?? 'null'));
}
setAreaKey(areaKey: string | null) {
@@ -62,7 +65,7 @@ export class UmbBlockGridEntriesContext
this.consumeContext(UMB_BLOCK_GRID_ENTRY_CONTEXT, (blockGridEntry) => {
this.#parentEntry = blockGridEntry;
this.#gotBlockParentEntry(); // is not used at this point.
this.#gotBlockParentEntry(); // is not used at this point. [NL]
}).asPromise();
this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
@@ -76,11 +79,12 @@ export class UmbBlockGridEntriesContext
blocks: this.#retrieveAllowedElementTypes(),
blockGroups: this._manager?.getBlockGroups() ?? [],
openClipboard: routingInfo.view === 'clipboard',
blockOriginData: { index: index },
blockOriginData: { index: index, areaKey: this.#areaKey, parentUnique: this.#parentUnique },
},
};
})
.observeRouteBuilder((routeBuilder) => {
// TODO: Does it make any sense that this is a state? Check usage and confirm. [NL]
this._catalogueRouteBuilderState.setValue(routeBuilder);
});
}
@@ -107,10 +111,6 @@ export class UmbBlockGridEntriesContext
);
}
/*#gotBlockParentEntry() {
if (!this.#parentEntry) return;
}*/
#gotAreaKey() {
if (this.#areaKey === undefined) return;
this.#gotBlockParentEntry();

View File

@@ -1,7 +1,7 @@
import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js';
import type { UmbBlockGridWorkspaceData } from '../index.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { type UmbBlockDataType, UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type';
@@ -40,13 +40,101 @@ export class UmbBlockGridManagerContext<
return super.createBlockData(contentElementTypeKey, partialLayoutEntry);
}
/**
* Inserts a layout entry into an area of a layout entry.
* @param layoutEntry The layout entry to insert.
* @param content The content data to insert.
* @param settings The settings data to insert.
* @param modalData The modal data.
* @returns a updated layout entries array if the insert was successful.
*
* @remarks
* This method is recursive and will search for the parentUnique in the layout entries.
* If the parentUnique is found, the layout entry will be inserted into the items of the area that matches the areaKey.
* This returns a new array of layout entries with the updated layout entry inserted.
* Because the layout entries are frozen, the affected parts is replaced with a new. Only updating/unfreezing the affected part of the structure.
*/
#appendLayoutEntryToArea(
insert: UmbBlockGridLayoutModel,
entries: Array<UmbBlockGridLayoutModel>,
parentId: string,
areaKey: string,
index: number,
): Array<UmbBlockGridLayoutModel> | undefined {
// I'm sorry, this code is not easy to read or maintain [NL]
let i: number = entries.length;
while (--i) {
const layoutEntry = entries[i];
if (layoutEntry.contentUdi === parentId) {
// Append the layout entry to be inserted and unfreeze the rest of the data:
return appendToFrozenArray(
entries,
{
...layoutEntry,
areas: layoutEntry.areas.map((x) =>
x.key === areaKey ? { ...x, items: appendToFrozenArray(x.items, insert) } : x,
),
},
(x) => x.contentUdi === layoutEntry.contentUdi,
);
}
let y: number = layoutEntry.areas?.length;
while (--y) {
// Recursively ask the items of this area to insert the layout entry, if something returns there was a match in this branch. [NL]
const correctedAreaItems = this.#appendLayoutEntryToArea(
insert,
layoutEntry.areas[y].items,
parentId,
areaKey,
index,
);
if (correctedAreaItems) {
// This area got a corrected set of items, lets append those to the area and unfreeze the surrounding data:
const area = layoutEntry.areas[y];
return appendToFrozenArray(
entries,
{
...layoutEntry,
areas: appendToFrozenArray(
layoutEntry.areas,
{ ...area, items: correctedAreaItems },
(z) => z.key === area.key,
),
},
(x) => x.contentUdi === layoutEntry.contentUdi,
);
}
}
}
// Find layout entry based on parentId, recursively, as it needs to check layout of areas as well:
return undefined;
}
insert(
layoutEntry: BlockLayoutType,
content: UmbBlockDataType,
settings: UmbBlockDataType | undefined,
modalData: UmbBlockGridWorkspaceData,
) {
this._layouts.appendOneAt(layoutEntry, modalData.originData.index ?? -1);
const index = modalData.originData.index ?? -1;
if (modalData.originData.parentUnique && modalData.originData.areaKey) {
// Find layout entry based on parentUnique, recursively, as it needs to check layout of areas as well:
const layoutEntries = this.#appendLayoutEntryToArea(
layoutEntry,
this._layouts.getValue(),
modalData.originData.parentUnique,
modalData.originData.areaKey,
index,
);
// If this appending was successful, we got a new set of layout entries which we can set as the new value: [NL]
if (layoutEntries) {
this._layouts.setValue(layoutEntries);
}
} else {
this._layouts.appendOneAt(layoutEntry, index);
}
this.insertBlockData(layoutEntry, content, settings, modalData);

View File

@@ -12,6 +12,7 @@ import {
} from '@umbraco-cms/backoffice/property-editor';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { incrementString } from '@umbraco-cms/backoffice/utils';
@customElement('umb-property-editor-ui-block-grid-areas-config')
export class UmbPropertyEditorUIBlockGridAreasConfigElement
@@ -96,6 +97,13 @@ export class UmbPropertyEditorUIBlockGridAreasConfigElement
this.#context.setLayoutColumns(this._areaGridColumns);
}
#generateUniqueAreaAlias(alias: string) {
while (this._value.find((area) => area.alias === alias)) {
alias = incrementString(alias);
}
return alias;
}
#addNewArea() {
if (!this._areaGridColumns) return;
const halfGridColumns = this._areaGridColumns * 0.5;
@@ -105,7 +113,7 @@ export class UmbPropertyEditorUIBlockGridAreasConfigElement
...this._value,
{
key: UmbId.new(),
alias: '', // TODO: Should we auto generate something here?
alias: this.#generateUniqueAreaAlias('area'),
columnSpan: columnSpan,
rowSpan: 1,
minAllowed: 0,

View File

@@ -60,20 +60,20 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
typeof UMB_WORKSPACE_MODAL.VALUE
>;
private _value: Array<UmbBlockTypeWithGroupKey> = [];
#value: Array<UmbBlockTypeWithGroupKey> = [];
@property({ attribute: false })
get value() {
return this._value;
return this.#value;
}
set value(value: Array<UmbBlockTypeWithGroupKey>) {
this._value = value ?? [];
this.#value = value ?? [];
this.#mapValuesToBlockGroups();
}
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
@state()
private _blockGroups: Array<UmbBlockGridTypeGroupType> = [];
#blockGroups?: Array<UmbBlockGridTypeGroupType>;
@state()
private _groupsWithBlockTypes: Array<MappedGroupWithBlockTypes> = [];
@@ -88,7 +88,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (instance) => {
this.#datasetContext = instance;
this.#observeProperties();
//this.#observeBlocks();
this.#observeBlockGroups();
});
this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
@@ -102,28 +103,34 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
});
}
async #observeProperties() {
async #observeBlockGroups() {
if (!this.#datasetContext) return;
this.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {
this._blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];
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
this._notGroupedBlockTypes = this._value.filter(
(block) => !block.groupKey || !this._blockGroups.find((group) => group.key === block.groupKey),
this._notGroupedBlockTypes = this.#value.filter(
(block) => !block.groupKey || !this.#blockGroups!.find((group) => group.key === block.groupKey),
);
// Map blocks to the group they belong to
this._groupsWithBlockTypes = this._blockGroups.map((group) => {
return { name: group.name, key: group.key, blocks: this._value.filter((value) => value.groupKey === group.key) };
this._groupsWithBlockTypes = this.#blockGroups.map((group) => {
return { name: group.name, key: group.key, blocks: this.#value.filter((value) => value.groupKey === group.key) };
});
this.#sorter.setModel(this._groupsWithBlockTypes);
@@ -131,7 +138,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
#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);
const filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);
this.value = [...filteredValues, ...updatedValues];
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
@@ -151,7 +158,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
: (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
const blocks = this.#value
.filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))
.filter(
(block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),
@@ -176,11 +183,11 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
// 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),
this.#blockGroups?.filter((group) => group.key !== groupKey),
);
// If a group is deleted, Move the blocks to no group:
this.value = this._value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));
this.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));
}
#changeGroupName(e: UUIInputEvent, groupKey: string) {
@@ -188,7 +195,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
// TODO: make one method for updating the blockGroupsDataSetValue:
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._blockGroups.map((group) => (group.key === groupKey ? { ...group, name: groupName } : group)),
this.#blockGroups?.map((group) => (group.key === groupKey ? { ...group, name: groupName } : group)),
);
}

View File

@@ -5,7 +5,8 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockGridWorkspaceData
extends UmbBlockWorkspaceData<{
index: number;
parentId: string | null;
parentUnique: string | null;
areaKey?: string;
}> {}
export const UMB_BLOCK_GRID_WORKSPACE_MODAL = new UmbModalToken<UmbBlockGridWorkspaceData, UmbWorkspaceValue>(
@@ -15,7 +16,7 @@ export const UMB_BLOCK_GRID_WORKSPACE_MODAL = new UmbModalToken<UmbBlockGridWork
type: 'sidebar',
size: 'medium',
},
data: { entityType: 'block', preset: {}, originData: { index: -1, parentId: null } },
data: { entityType: 'block', preset: {}, originData: { index: -1, parentUnique: null } },
// Recast the type, so the entityType data prop is not required:
},
) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;