diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts index dea6494f106f..70206bc8f333 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts @@ -216,6 +216,7 @@ export class UmbBlockGridManagerContext< ) { this.setOneLayout(layoutEntry, originData); this.insertBlockData(layoutEntry, content, settings, originData); + this.notifyBlockInserted(layoutEntry, originData); return true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts index 2d1dc222588b..5efe02fe33e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts @@ -226,6 +226,7 @@ export class UmbBlockGridEntriesContext areaKey: this.#areaKey, parentUnique: this.#parentUnique, } as UmbBlockGridWorkspaceOriginData, + // TODO: Check if we use this for anything. I think its not possible to configure inline editing for block grid? [NL] createBlockInWorkspace: this._manager.getInlineEditingMode() === false, }, }; @@ -440,7 +441,23 @@ export class UmbBlockGridEntriesContext } getPathForCreateBlock(index: number) { - return this._catalogueRouteBuilderState.getValue()?.({ view: 'create', index: index }); + const pathBuilder = this._catalogueRouteBuilderState.getValue(); + if (!pathBuilder) return undefined; + + const blockTypes = this.#allowedBlockTypes.getValue(); + if (blockTypes?.length === 1) { + const elementKey = blockTypes[0].contentElementTypeKey; + + if (!this._manager) return undefined; + // does the Block have any Content properties? + const contentTypeKey = this._manager.getContentTypeKeyOfContentKey(elementKey); + if (contentTypeKey && this._manager.getContentTypeHasProperties(contentTypeKey) === false) { + return undefined; + } + return pathBuilder({ view: 'create', index: index }) + 'modal/umb-modal-workspace/create/' + elementKey; + } + + return pathBuilder({ view: 'create', index: index }); } getPathForClipboard(index: number) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index b12673e6558e..d055f0e322fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -1,7 +1,18 @@ import type { UmbBlockGridEntryElement } from '../block-grid-entry/block-grid-entry.element.js'; -import type { UmbBlockGridLayoutModel } from '../../types.js'; +import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../../types.js'; +import type { UmbBlockGridWorkspaceOriginData } from '../../workspace/block-grid-workspace.modal-token.js'; import { UmbBlockGridEntriesContext } from './block-grid-entries.context.js'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + repeat, + state, + when, +} from '@umbraco-cms/backoffice/external/lit'; import { getAccumulatedValueOfIndex, getInterpolatedIndexOfPositionInWeightMap, @@ -174,7 +185,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen private _areaKey?: string | null; @state() - private _canCreate?: boolean; + private _allowedBlockTypes?: UmbBlockGridTypeModel[]; @state() private _configCreateLabel?: string; @@ -208,10 +219,10 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen null, ); this.observe( - this.#context.amountOfAllowedBlockTypes, - (length) => { - this._canCreate = length > 0; - if (length === 1) { + this.#context.allowedBlockTypes, + (allowedBlockTypes) => { + this._allowedBlockTypes = allowedBlockTypes; + if (allowedBlockTypes.length === 1) { this.observe( this.#context.firstAllowedBlockTypeName(), (firstAllowedName) => { @@ -400,7 +411,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen `, )} - ${when(this._canCreate, () => this.#renderCreateButtonGroup())} + ${when(this._allowedBlockTypes && this._allowedBlockTypes.length > 0, () => this.#renderCreateButtonGroup())} ${when(this._areaKey, () => html``)} `; } @@ -426,13 +437,43 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen #renderCreateButton() { if (this._isReadOnly && this._layoutEntries.length > 0) return nothing; + const createPath = this.#context.getPathForCreateBlock(-1); return html` { + // If no path, then we can conclude there is not modal flow for the user to follow, instead we will just insert the Block: [NL] + if (createPath === undefined) { + if (!this._allowedBlockTypes || this._allowedBlockTypes.length === 0) { + throw new Error('No block types are configured for this Block List property editor'); + } + const areaKey = this._areaKey; + const parentUnique = this.#context.getParentUnique(); + if (areaKey === undefined || parentUnique === undefined) { + throw new Error('Cannot create block without a defined areaKey and parentUnique'); + } + const originData: UmbBlockGridWorkspaceOriginData = { + index: -1, + areaKey: areaKey ?? undefined, + parentUnique: parentUnique, + }; + const created = await this.#context.create( + this._allowedBlockTypes[0].contentElementTypeKey, + // We can parse an empty object, cause the rest will be filled in by others. + {} as any, + originData, + ); + if (created) { + this.#context.insert(created.layout, created.content, created.settings, originData); + } else { + throw new Error('Failed to create block'); + } + } + }} ?disabled=${this._isReadOnly}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts index 8bb2f9fbcfaa..2b7feb3f0fde 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts @@ -1,5 +1,10 @@ import { UMB_BLOCK_LIST_ENTRY_CONTEXT } from '../../context/index.js'; -import { UMB_BLOCK_WORKSPACE_ALIAS } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockListLayoutModel, UmbBlockListWorkspaceOriginData } from '../../index.js'; +import { + UMB_BLOCK_MANAGER_CONTEXT, + UMB_BLOCK_WORKSPACE_ALIAS, + UmbBlockInsertedEvent, +} from '@umbraco-cms/backoffice/block'; import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbExtensionApiInitializer, UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -10,6 +15,8 @@ import type { UmbApiConstructorArgumentsMethodType } from '@umbraco-cms/backoffi import type { UmbBlockDataType, UMB_BLOCK_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/block'; import '../../../block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.js'; +import { UmbContextBoundary } from '@umbraco-cms/backoffice/context-api'; +import { UMB_VIEW_CONTEXT } from '@umbraco-cms/backoffice/view'; const apiArgsCreator: UmbApiConstructorArgumentsMethodType = (manifest: unknown) => { return [{ manifest }]; @@ -20,6 +27,7 @@ const apiArgsCreator: UmbApiConstructorArgumentsMethodType = (manifest: */ @customElement('umb-inline-list-block') export class UmbInlineListBlockElement extends UmbLitElement { + #manager?: typeof UMB_BLOCK_MANAGER_CONTEXT.TYPE; #blockContext?: typeof UMB_BLOCK_LIST_ENTRY_CONTEXT.TYPE; #workspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; #contentKey?: string; @@ -69,6 +77,17 @@ export class UmbInlineListBlockElement extends UmbLitElement { ); }); + this.consumeContext(UMB_BLOCK_MANAGER_CONTEXT, (manager) => { + if (this.#manager) { + this.#manager.removeEventListener(UmbBlockInsertedEvent.TYPE, this.#onBlockInserted); + } + this.#manager = manager; + this.#manager?.addEventListener(UmbBlockInsertedEvent.TYPE, this.#onBlockInserted); + }); + + // Block the access to the View Context for this inline block workspace: [NL] + new UmbContextBoundary(this, UMB_VIEW_CONTEXT); + new UmbExtensionApiInitializer( this, umbExtensionsRegistry, @@ -79,6 +98,9 @@ export class UmbInlineListBlockElement extends UmbLitElement { if (permitted && context) { this.#workspaceContext = context; this.#workspaceContext.establishLiveSync(); + // Avoid view context becoming active: [NL] + // in this case its not a routable workspace and we do not want it to become an active view, appending shortcuts or setting browser title. (maybe this code needs to be more explicit. Like a inlineMode()?) [NL] + this.#workspaceContext.view.destroy(); this.#workspaceContext.autoReportValidation(); this.#load(); @@ -127,6 +149,13 @@ export class UmbInlineListBlockElement extends UmbLitElement { this.#workspaceContext.load(this.#contentKey); } + #onBlockInserted = (event: Event) => { + const blockEvent = event as UmbBlockInsertedEvent; + if (blockEvent.detail.layout.contentKey === this.#contentKey) { + this._isOpen = true; + } + }; + #expose = () => { this.#workspaceContext?.expose(); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts index 4280c8ae6e93..8f70d5ba90c8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts @@ -107,7 +107,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< data.originData as UmbBlockListWorkspaceOriginData, ); if (created) { - this.insert( + await this.insert( created.layout, created.content, created.settings, @@ -167,7 +167,27 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< } getPathForCreateBlock(index: number) { - return this._catalogueRouteBuilderState.getValue()?.({ view: 'create', index: index }); + const pathBuilder = this._catalogueRouteBuilderState.getValue(); + if (!pathBuilder) return undefined; + + if (!this._manager) return undefined; + const blockTypes = this._manager.getBlockTypes(); + if (blockTypes.length === 1) { + const elementKey = blockTypes[0].contentElementTypeKey; + if (this._manager.getInlineEditingMode()) { + return undefined; + } + + // does the Block have any Content properties? + const contentTypeKey = this._manager.getContentTypeKeyOfContentKey(elementKey); + if (contentTypeKey && this._manager.getContentTypeHasProperties(contentTypeKey) === false) { + return undefined; + } + + return pathBuilder?.({ view: 'create', index: index }) + 'modal/umb-modal-workspace/create/' + elementKey; + } + + return pathBuilder?.({ view: 'create', index: index }); } getPathForClipboard(index: number) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts index 0f69421b7b14..973d1cac2ea5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts @@ -67,8 +67,8 @@ export class UmbBlockListManagerContext< originData: UmbBlockListWorkspaceOriginData, ) { this._layouts.appendOneAt(layoutEntry, originData.index ?? -1); - this.insertBlockData(layoutEntry, content, settings, originData); + this.notifyBlockInserted(layoutEntry, originData); return true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index d76582678f67..fa80394f41b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -3,7 +3,17 @@ import { UmbBlockListEntriesContext } from '../../context/block-list-entries.con import type { UmbBlockListLayoutModel, UmbBlockListValueModel } from '../../types.js'; import type { UmbBlockListEntryElement } from '../../components/block-list-entry/index.js'; import { UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS } from './constants.js'; -import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element'; +import { + html, + customElement, + property, + state, + repeat, + css, + nothing, + ifDefined, +} from '@umbraco-cms/backoffice/external/lit'; import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; import { extractJsonQueryProps, @@ -12,7 +22,6 @@ import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, } from '@umbraco-cms/backoffice/validation'; import { jsonStringComparison, observeMultiple } from '@umbraco-cms/backoffice/observable-api'; -import { umbDestroyOnDisconnect, UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; @@ -419,32 +428,55 @@ export class UmbPropertyEditorUIBlockListElement #renderInlineCreateButton(index: number) { if (this.readonly) return nothing; + const createPath = this.#entriesContext.getPathForCreateBlock(index); return html` + href=${ifDefined(createPath)} + @click=${() => { + // If no path, then we can conclude there is not modal flow for the user to follow, instead we will just insert the Block: [NL] + if (createPath === undefined) { + this.#handleCreateWithNoCreatePath(index); + } + }}> `; } #renderCreateButton() { - let createPath: string | undefined; - if (this._blocks?.length === 1) { - const elementKey = this._blocks[0].contentElementTypeKey; - createPath = - this._catalogueRouteBuilder?.({ view: 'create', index: -1 }) + 'modal/umb-modal-workspace/create/' + elementKey; - } else { - createPath = this._catalogueRouteBuilder?.({ view: 'create', index: -1 }); - } + if (!this._catalogueRouteBuilder) return nothing; + const createPath = this.#entriesContext.getPathForCreateBlock(-1); return html` + .label=${this._createButtonLabel} + href=${ifDefined(createPath)} + ?disabled=${this.readonly} + @click=${() => { + // If no path, then we can conclude there is not modal flow for the user to follow, instead we will just insert the Block: [NL] + if (createPath === undefined) { + this.#handleCreateWithNoCreatePath(); + } + }}> `; } + async #handleCreateWithNoCreatePath(index?: number) { + if (!this._blocks || this._blocks.length === 0) { + throw new Error('No block types are configured for this Block List property editor'); + } + if (index === undefined) { + index = -1; + } + const originData = { index }; + const created = await this.#entriesContext.create(this._blocks[0].contentElementTypeKey, {}, originData); + if (created) { + this.#entriesContext.insert(created.layout, created.content, created.settings, originData); + } else { + throw new Error('Failed to create block'); + } + } + #renderPasteButton() { return html` + href=${ifDefined(createPath)} + ?disabled=${this.readonly} + @click=${async () => { + // If no path, then we can conclude there is not modal flow for the user to follow, instead we will just insert the Block: [NL] + if (createPath === undefined) { + if (!this._blocks || this._blocks.length === 0) { + throw new Error('No block types are configured for this Block List property editor'); + } + const originData = { index: -1 }; + const created = await this.#entriesContext.create(this._blocks[0].contentElementTypeKey, {}, originData); + if (created) { + this.#entriesContext.insert(created.layout, created.content, created.settings, originData); + } else { + throw new Error('Failed to create block'); + } + } + }}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 1068455fab87..3e391c0d9020 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -1,5 +1,6 @@ import type { UmbBlockWorkspaceOriginData } from '../workspace/index.js'; import type { UmbBlockLayoutBaseModel, UmbBlockDataModel, UmbBlockExposeModel } from '../types.js'; +import { UmbBlockInsertedEvent } from '../events/block-inserted.event.js'; import { UMB_BLOCK_MANAGER_CONTEXT } from './block-manager.context-token.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -515,6 +516,17 @@ export abstract class UmbBlockManagerContext< this.#setInitialBlockExpose(content); } + protected async notifyBlockInserted(layoutEntry: BlockLayoutType, originData: BlockOriginDataType) { + // Await one rendering frame, to make sure new Block has been rendered at the time of the Event firing. [NL] + await new Promise((resolve) => requestAnimationFrame(() => resolve(true))); + this.dispatchEvent( + new UmbBlockInsertedEvent({ + originData, + layout: layoutEntry, + }), + ); + } + async #setInitialBlockExpose(content: UmbBlockDataModel) { await this.contentTypesLoaded; const contentStructure = this.getStructure(content.contentTypeKey); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/events/block-inserted.event.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/events/block-inserted.event.ts new file mode 100644 index 000000000000..8ca4dffca1c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/events/block-inserted.event.ts @@ -0,0 +1,26 @@ +import type { UmbBlockLayoutBaseModel } from '../types.js'; + +export interface UmbBlockInsertedEventDetail< + TLayoutModel extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel, + TOriginData = unknown, +> { + originData: TOriginData; + layout: TLayoutModel; +} + +export class UmbBlockInsertedEvent< + TLayoutModel extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel, + TOriginData = unknown, +> extends CustomEvent> { + static readonly TYPE = 'umb-internal:blockInserted'; + + constructor(detail: UmbBlockInsertedEventDetail) { + super(UmbBlockInsertedEvent.TYPE, { detail, bubbles: false, composed: false, cancelable: false }); + } +} + +declare global { + interface GlobalEventHandlersEventMap { + [UmbBlockInsertedEvent.TYPE]: UmbBlockInsertedEvent; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/events/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/events/index.ts new file mode 100644 index 000000000000..df4caf92a8d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/events/index.ts @@ -0,0 +1 @@ +export * from './block-inserted.event.js'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/index.ts index 497575eb46c6..6bbeb337339d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/index.ts @@ -1,6 +1,7 @@ export * from './clipboard/index.js'; export * from './components/index.js'; export * from './context/index.js'; +export * from './events/index.js'; export * from './modals/index.js'; export * from './property-value-cloner/index.js'; export * from './property-value-resolver/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index e0352a53b04f..2d23365155d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -91,7 +91,7 @@ export class UmbBlockWorkspaceContext { this.#blockManager = manager;