create block at a decired index

This commit is contained in:
Niels Lyngsø
2024-01-22 21:55:16 +01:00
parent 1f97864609
commit 86c57bf113
15 changed files with 210 additions and 44 deletions

View File

@@ -1,4 +1,5 @@
import { partialUpdateFrozenArray } from '../utils/partial-update-frozen-array.function.js';
import { pushAtToUniqueArray } from '../utils/push-at-to-unique-array.function.js';
import { pushToUniqueArray } from '../utils/push-to-unique-array.function.js';
import { UmbDeepState } from './deep-state.js';
@@ -166,6 +167,33 @@ export class UmbArrayState<T> extends UmbDeepState<T[]> {
return this;
}
/**
* @method appendOneAt
* @param {T} entry - new data to be added in this Subject.
* @param {T} index - index of where to append this data into the Subject.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Append some new data to this Subject.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 3, value: 'bar'}
* ];
* const myState = new UmbArrayState(data);
* myState.appendOneAt({ key: 2, value: 'in-between'}, 1);
*/
appendOneAt(entry: T, index: number) {
const next = [...this.getValue()];
if (this.getUniqueMethod) {
pushAtToUniqueArray(next, entry, this.getUniqueMethod, index);
} else if (index === -1 || index >= next.length) {
next.push(entry);
} else {
next.splice(index, 0, entry);
}
this.setValue(next);
return this;
}
/**
* @method append
* @param {T[]} entries - A array of new data to be added in this Subject.

View File

@@ -0,0 +1,29 @@
/**
* @export
* @method pushToUniqueArray
* @param {T[]} data - An array of objects.
* @param {T} entry - The object to insert or replace with.
* @param {getUniqueMethod: (entry: T) => unknown} [getUniqueMethod] - Method to get the unique value of an entry.
* @description - Append or replaces an item of an Array.
* @example <caption>Example append new entry for a Array. Where the key is unique and the item will be updated if matched with existing.</caption>
* const entry = {key: 'myKey', value: 'myValue'};
* const newDataSet = pushToUniqueArray([], entry, x => x.key === key, 1);
* mySubject.next(newDataSet);
*/
export function pushAtToUniqueArray<T>(
data: T[],
entry: T,
getUniqueMethod: (entry: T) => unknown,
index: number,
): T[] {
const unique = getUniqueMethod(entry);
const indexToReplace = data.findIndex((x) => getUniqueMethod(x) === unique);
if (indexToReplace !== -1) {
data[indexToReplace] = entry;
} else if (index === -1 || index >= data.length) {
data.push(entry);
} else {
data.splice(index, 0, entry);
}
return data;
}

View File

@@ -0,0 +1,20 @@
import { UmbBlockWorkspaceData } from '../../index.js';
import { UmbModalToken, UmbWorkspaceData, UmbWorkspaceValue } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockListWorkspaceData
extends UmbBlockWorkspaceData<{
index: number;
parentId: string | null;
}> {}
export const UMB_BLOCK_GRID_WORKSPACE_MODAL = new UmbModalToken<UmbBlockListWorkspaceData, UmbWorkspaceValue>(
'Umb.Modal.Workspace',
{
modal: {
type: 'sidebar',
size: 'medium',
},
data: { entityType: 'block', preset: {}, originData: { index: -1, parentId: null } },
// Recast the type, so the entityType data prop is not required:
},
) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;

View File

@@ -1 +1,2 @@
export const UMB_BLOCK_GRID_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.BlockGridType';
export * from './block-grid-workspace.modal-token.js';

View File

@@ -1,5 +1,6 @@
import type { UmbBlockListLayoutModel, UmbBlockListTypeModel } from '../types.js';
import { UmbBlockManagerContext } from '../../block/manager/block-manager.context.js';
import type { UmbBlockListWorkspaceData } from '../index.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
@@ -17,10 +18,12 @@ export class UmbBlockListManagerContext<
this.#inlineEditingMode.setValue(inlineEditingMode ?? false);
}
createBlock(layoutEntry: Omit<BlockLayoutType, 'contentUdi'>, contentElementTypeKey: string) {
_createBlock(modalData: UmbBlockListWorkspaceData, layoutEntry: BlockLayoutType, contentElementTypeKey: string) {
// Here is room to append some extra layout properties if needed for this type.
return super._createBlockData(layoutEntry, contentElementTypeKey);
this._layouts.appendOneAt(layoutEntry, modalData.originData.index ?? -1);
return true;
}
}

View File

@@ -136,9 +136,16 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
});
new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
.addAdditionalPath(':view')
.addAdditionalPath(':view/:index')
.onSetup((routingInfo) => {
return { data: { blocks: this._blocks ?? [], openClipboard: routingInfo.view === 'clipboard' } };
const index = routingInfo.index ? parseInt(routingInfo.index) : -1;
return {
data: {
blocks: this._blocks ?? [],
openClipboard: routingInfo.view === 'clipboard',
blockOriginData: { index: index },
},
};
})
.observeRouteBuilder((routeBuilder) => {
this._catalogueRouteBuilder = routeBuilder;
@@ -149,8 +156,9 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
return html` ${repeat(
this._layouts,
(x) => x.contentUdi,
(layoutEntry) =>
html`<uui-button-inline-create></uui-button-inline-create>
(layoutEntry, index) =>
html`<uui-button-inline-create
href=${this._catalogueRouteBuilder?.({ view: 'create', index: index }) ?? ''}></uui-button-inline-create>
<umb-property-editor-ui-block-list-block data-udi=${layoutEntry.contentUdi} .layout=${layoutEntry}>
</umb-property-editor-ui-block-list-block> `,
)}
@@ -159,13 +167,13 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
id="add-button"
look="placeholder"
label=${this.localize.term('content_createEmpty')}
href=${this._catalogueRouteBuilder?.({ view: 'create' }) ?? ''}>
href=${this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) ?? ''}>
${this.localize.term('content_createEmpty')}
</uui-button>
<uui-button
label=${this.localize.term('content_createFromClipboard')}
look="placeholder"
href=${this._catalogueRouteBuilder?.({ view: 'clipboard' }) ?? ''}>
href=${this._catalogueRouteBuilder?.({ view: 'clipboard', index: -1 }) ?? ''}>
<uui-icon name="icon-paste-in"></uui-icon>
</uui-button>
</uui-button-group>`;

View File

@@ -0,0 +1,19 @@
import { UmbBlockWorkspaceData } from '../../index.js';
import { UmbModalToken, UmbWorkspaceData, UmbWorkspaceValue } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockListWorkspaceData
extends UmbBlockWorkspaceData<{
index: number;
}> {}
export const UMB_BLOCK_LIST_WORKSPACE_MODAL = new UmbModalToken<UmbBlockListWorkspaceData, UmbWorkspaceValue>(
'Umb.Modal.Workspace',
{
modal: {
type: 'sidebar',
size: 'medium',
},
data: { entityType: 'block', preset: {}, originData: { index: -1 } },
// Recast the type, so the entityType data prop is not required:
},
) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;

View File

@@ -1 +1,2 @@
export const UMB_BLOCK_LIST_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.BlockListType';
export * from './block-list-workspace.modal-token.js';

View File

@@ -0,0 +1,19 @@
import { UmbBlockWorkspaceData } from '../../index.js';
import { UmbModalToken, UmbWorkspaceData, UmbWorkspaceValue } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockListWorkspaceData
extends UmbBlockWorkspaceData<{
index: number;
}> {}
export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken<UmbBlockListWorkspaceData, UmbWorkspaceValue>(
'Umb.Modal.Workspace',
{
modal: {
type: 'sidebar',
size: 'medium',
},
data: { entityType: 'block', preset: {}, originData: { index: -1 } },
// Recast the type, so the entityType data prop is not required:
},
) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;

View File

@@ -1 +1,2 @@
export const UMB_BLOCK_RTE_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.BlockRteType';
export * from './block-rte-workspace.modal-token.js';

View File

@@ -8,6 +8,7 @@ import {
UMB_BLOCK_MANAGER_CONTEXT,
UMB_BLOCK_WORKSPACE_MODAL,
UmbBlockTypeBaseModel,
UmbBlockWorkspaceData,
} from '@umbraco-cms/backoffice/block';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type';
@@ -36,8 +37,8 @@ export abstract class UmbBlockManagerContext<
#editorConfiguration = new UmbClassState<UmbPropertyEditorConfigCollection | undefined>(undefined);
public readonly editorConfiguration = this.#editorConfiguration.asObservable();
#layouts = new UmbArrayState(<Array<BlockLayoutType>>[], (x) => x.contentUdi);
public readonly layouts = this.#layouts.asObservable();
protected _layouts = new UmbArrayState(<Array<BlockLayoutType>>[], (x) => x.contentUdi);
public readonly layouts = this._layouts.asObservable();
#contents = new UmbArrayState(<Array<UmbBlockDataType>>[], (x) => x.udi);
public readonly contents = this.#contents.asObservable();
@@ -57,7 +58,7 @@ export abstract class UmbBlockManagerContext<
}
setLayouts(layouts: Array<BlockLayoutType>) {
this.#layouts.setValue(layouts);
this._layouts.setValue(layouts);
}
setContents(contents: Array<UmbBlockDataType>) {
@@ -116,7 +117,7 @@ export abstract class UmbBlockManagerContext<
}
layoutOf(contentUdi: string) {
return this.#layouts.asObservablePart((source) => source.find((x) => x.contentUdi === contentUdi));
return this._layouts.asObservablePart((source) => source.find((x) => x.contentUdi === contentUdi));
}
contentOf(udi: string) {
return this.#contents.asObservablePart((source) => source.find((x) => x.udi === udi));
@@ -126,7 +127,7 @@ export abstract class UmbBlockManagerContext<
}
setOneLayout(layoutData: BlockLayoutType) {
return this.#layouts.appendOne(layoutData);
return this._layouts.appendOne(layoutData);
}
setOneContent(contentData: UmbBlockDataType) {
this.#contents.appendOne(contentData);
@@ -135,9 +136,17 @@ export abstract class UmbBlockManagerContext<
this.#settings.appendOne(settingsData);
}
abstract createBlock(layoutEntry: Omit<BlockLayoutType, 'contentUdi'>, contentElementTypeKey: string): boolean;
abstract _createBlock(
modalData: UmbBlockWorkspaceData,
layoutEntry: Omit<BlockLayoutType, 'contentUdi'>,
contentElementTypeKey: string,
): boolean;
protected _createBlockData(layoutEntry: Omit<BlockLayoutType, 'contentUdi'>, contentElementTypeKey: string) {
public createBlock(
modalData: UmbBlockWorkspaceData,
layoutEntry: Omit<BlockLayoutType, 'contentUdi'>,
contentElementTypeKey: string,
) {
// Find block type.
const blockType = this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentElementTypeKey);
if (!blockType) {
@@ -154,7 +163,9 @@ export abstract class UmbBlockManagerContext<
fullLayoutEntry.settingsUdi = buildUdi('element', UmbId.new());
}
this.#layouts.appendOne(fullLayoutEntry);
if (this._createBlock(modalData, fullLayoutEntry, contentElementTypeKey) === false) {
return false;
}
// Create content entry:
if (fullLayoutEntry.contentUdi) {
@@ -184,14 +195,14 @@ export abstract class UmbBlockManagerContext<
}
deleteBlock(contentUdi: string) {
const layout = this.#layouts.value.find((x) => x.contentUdi === contentUdi);
const layout = this._layouts.value.find((x) => x.contentUdi === contentUdi);
if (!layout) return;
if (layout.settingsUdi) {
this.#settings.removeOne(layout.settingsUdi);
}
this.#layouts.removeOne(contentUdi);
this._layouts.removeOne(contentUdi);
this.#contents.removeOne(contentUdi);
}
}

View File

@@ -6,7 +6,11 @@ import {
} from '@umbraco-cms/backoffice/block';
import { css, html, customElement, state, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { groupBy } from '@umbraco-cms/backoffice/external/lodash';
import { UmbModalBaseElement, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import {
UMB_MODAL_CONTEXT_TOKEN,
UmbModalBaseElement,
UmbModalRouteRegistrationController,
} from '@umbraco-cms/backoffice/modal';
@customElement('umb-block-catalogue-modal')
export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
@@ -28,18 +32,22 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
constructor() {
super();
new UmbModalRouteRegistrationController(this, UMB_BLOCK_WORKSPACE_MODAL)
//.addAdditionalPath('block') // No need for additional path specification in this context as this is for sure the only workspace we want to open here.
.onSetup(() => {
return { data: { preset: {} } };
})
.onSubmit(() => {
// When workspace is submitted, we want to close this modal.
this.modalContext?.submit();
})
.observeRouteBuilder((routeBuilder) => {
this._workspacePath = routeBuilder({});
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (modalContext) => {
new UmbModalRouteRegistrationController(this, UMB_BLOCK_WORKSPACE_MODAL)
//.addAdditionalPath('block') // No need for additional path specification in this context as this is for sure the only workspace we want to open here.
.onSetup(() => {
return {
data: { preset: {}, originData: (modalContext.data as UmbBlockCatalogueModalData).blockOriginData },
};
})
.onSubmit(() => {
// When workspace is submitted, we want to close this modal.
this.modalContext?.submit();
})
.observeRouteBuilder((routeBuilder) => {
this._workspacePath = routeBuilder({});
});
});
}
connectedCallback() {

View File

@@ -1,10 +1,11 @@
import { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block';
import type { UmbBlockTypeBaseModel, UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockCatalogueModalData {
blocks: Array<UmbBlockTypeBaseModel>;
blockGroups?: Array<{ name: string; key: string }>;
openClipboard?: boolean;
blockOriginData: UmbBlockWorkspaceData;
}
export type UmbBlockCatalogueModalValue = never;

View File

@@ -5,7 +5,7 @@ import { UmbBooleanState, UmbObjectState, UmbStringState } from '@umbraco-cms/ba
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UMB_BLOCK_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/block';
import { UMB_BLOCK_MANAGER_CONTEXT, UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block';
import { buildUdi } from '@umbraco-cms/backoffice/utils';
import { UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
@@ -18,8 +18,9 @@ export class UmbBlockWorkspaceContext<
readonly workspaceAlias;
#blockManager?: typeof UMB_BLOCK_MANAGER_CONTEXT.TYPE;
#retrieveModalContext;
#retrieveBlockManager;
#modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE;
#retrieveModalContext;
#editorConfigPromise?: Promise<unknown>;
#entityType: string;
@@ -49,6 +50,7 @@ export class UmbBlockWorkspaceContext<
this.workspaceAlias = workspaceArgs.manifest.alias;
this.#retrieveModalContext = this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (context) => {
this.#modalContext = context;
context.onSubmit().catch(this.#modalRejected);
}).asPromise();
@@ -111,13 +113,17 @@ export class UmbBlockWorkspaceContext<
async create(contentElementTypeId: string) {
await this.#retrieveBlockManager;
await this.#retrieveModalContext;
if (!this.#blockManager) {
throw new Error('Block manager not found');
throw new Error('Block Manager not found');
return;
}
if (!this.#modalContext) {
throw new Error('Modal Context not found');
return;
}
//
// TODO: Condense this into some kind of create method?
const key = UmbId.new();
const contentUdi = buildUdi('block', key);
const layoutData: UmbBlockLayoutBaseModel = {
@@ -135,7 +141,11 @@ export class UmbBlockWorkspaceContext<
this.#layout.setValue(layoutData as LayoutDataType);
if (this.#liveEditingMode) {
const blockCreated = this.#blockManager.createBlock(layoutData, contentElementTypeId);
const blockCreated = this.#blockManager.createBlock(
this.#modalContext.data as UmbBlockWorkspaceData,
layoutData,
contentElementTypeId,
);
if (!blockCreated) {
throw new Error('Block Manager could not create block');
return;

View File

@@ -1,10 +1,17 @@
import { UmbModalToken, UmbWorkspaceData, UmbWorkspaceValue } from '@umbraco-cms/backoffice/modal';
export const UMB_BLOCK_WORKSPACE_MODAL = new UmbModalToken<UmbWorkspaceData, UmbWorkspaceValue>('Umb.Modal.Workspace', {
modal: {
type: 'sidebar',
size: 'large',
export interface UmbBlockWorkspaceData<OriginDataType = unknown> extends UmbWorkspaceData {
originData: OriginDataType;
}
export const UMB_BLOCK_WORKSPACE_MODAL = new UmbModalToken<UmbBlockWorkspaceData, UmbWorkspaceValue>(
'Umb.Modal.Workspace',
{
modal: {
type: 'sidebar',
size: 'large',
},
data: { entityType: 'block', preset: {}, originData: {} },
// Recast the type, so the entityType data prop is not required:
},
data: { entityType: 'block', preset: {} },
// Recast the type, so the entityType data prop is not required:
}) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;
) as UmbModalToken<Omit<UmbWorkspaceData, 'entityType'>, UmbWorkspaceValue>;