Trying to render a umb-property-editor-ui-block-list on a custom workspace panel

Im trying to place a umb-property-editor-ui-block-list editor on a workplace panel in a custom section.

I can get the Add button to display (only by going back to content and a page that hase a BlockList omponent on it. Then returning to my section panel).

This is what I have

`// Start with an empty BlockEditorModel
value = {
    layout: [],
    contentData: [],
    settingsData: []
};`

`` connectedCallback() {
super.connectedCallback();

    // Wait for first render so the workspace shell exists
    this.loadBlockListConfig();
}



/* -------------------------------------------------------------
   LOAD BLOCK LIST CONFIG (datatype definition)
   ------------------------------------------------------------- */
private async loadBlockListConfig() {
    const response = await umbHttpClient.get({
        url: `/umbraco/management/api/v1/data-type/55e53032-de54-49ae-9232-c79fcfa91c4e`,
        security: [{ scheme: "bearer", type: "http" }]
    });

    this.config = (response.data as any).values;
    console.log("🔵 BlockList Config:", this.config);
} ``

then I render it

`` render() {
    return html`
        <h2>Calendar Group</h2>
        <umb-property-editor-ui-block-list
            .value=${this.value}
            .config=${this.config}
            @umb-property-value-change=${this.onValueChange}>
        </umb-property-editor-ui-block-list>
    `;
} ``

like I said I can get the add button to render but it comes with errors

property-editor-ui-block-list.element.ts:105 Uncaught (in promise) TypeError: t.getValueByAlias is not a function
at set config (property-editor-ui-block-list.element.ts:105:34)
at Nt.j (lit-html.js:6:6006)
at Nt.$AI (lit-html.js:6:5815)
at Rt.p (lit-html.js:6:3601)
at U.$ (lit-html.js:6:4600)
at U.
$AI (lit-html.js:6:4152)
at Dt (lit-html.js:6:7083)
at _GenesisCalendarGroupsWorkspace.update (lit-element.js:6:356)
at _GenesisCalendarGroupsWorkspace.performUpdate (reactive-element.js:6:5255)
at _GenesisCalendarGroupsWorkspace.scheduleUpdate (reactive-element.js:6:4775)
config @ property-editor-ui-block-list.element.ts:105
j @ lit-html.js:6
_$AI @ lit-html.js:6
p @ lit-html.js:6
$ @ lit-html.js:6
_$AI @ lit-html.js:6
(anonymous) @ lit-html.js:6
update @ lit-element.js:6
performUpdate @ reactive-element.js:6
scheduleUpdate @ reactive-element.js:6
_$EP @ reactive-element.js:6
await in _$EP
requestUpdate @ reactive-element.js:6
set @ reactive-element.js:6
loadBlockListConfig @ Genesis-Calendar.esm.js:2298
await in loadBlockListConfig
connectedCallback @ Genesis-Calendar.esm.js:2288
O @ lit-html.js:6
T @ lit-html.js:6
_$AI @ lit-html.js:6
(anonymous) @ lit-html.js:6
update @ lit-element.js:6
performUpdate @ reactive-element.js:6
scheduleUpdate @ reactive-element.js:6
_$EP @ reactive-element.js:6
await in _$EP
requestUpdate @ reactive-element.js:6
set @ reactive-element.js:6
(anonymous) @ workspace.element.ts:46
(anonymous) @ base-extensions-initializer.controller.ts:186
requestAnimationFrame
(anonymous) @ base-extensions-initializer.controller.ts:146
(anonymous) @ base-extension-initializer.controller.ts:329
await in (anonymous)
#gotManifest @ base-extension-initializer.controller.ts:169
(anonymous) @ base-extension-initializer.controller.ts:83
(anonymous) @ Subscriber.js:96
(anonymous) @ Subscriber.js:63
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ switchMap.js:14
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ innerFrom.js:51
(anonymous) @ Observable.js:38
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ switchMap.js:14
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ BehaviorSubject.js:19
(anonymous) @ Observable.js:38
(anonymous) @ Subject.js:95
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ Observable.js:66
(anonymous) @ Observable.js:30
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ switchMap.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
constructor @ observer.ts:16
constructor @ observer.controller.ts:19
observe @ class.mixin.ts:70
_init @ base-extension-initializer.controller.ts:72
constructor @ extension-element-and-api-initializer.controller.ts:115
_createController @ extensions-element-and-api-initializer.controller.ts:80
(anonymous) @ base-extensions-initializer.controller.ts:120
(anonymous) @ base-extensions-initializer.controller.ts:117
(anonymous) @ Subscriber.js:96
(anonymous) @ Subscriber.js:63
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ combineLatest.js:47
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ BehaviorSubject.js:19
(anonymous) @ Observable.js:38
(anonymous) @ Subject.js:95
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ Observable.js:66
(anonymous) @ Observable.js:30
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ combineLatest.js:40
maybeSchedule @ combineLatest.js:67
(anonymous) @ combineLatest.js:37
(anonymous) @ combineLatest.js:57
maybeSchedule @ combineLatest.js:67
(anonymous) @ combineLatest.js:31
(anonymous) @ Observable.js:38
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
constructor @ observer.ts:16
constructor @ observer.controller.ts:19
observe @ class.mixin.ts:70
_init @ base-extensions-initializer.controller.ts:86
constructor @ extensions-element-and-api-initializer.controller.ts:76
(anonymous) @ workspace.element.ts:39
entityType @ workspace.element.ts:31
set @ reactive-element.js:6
setup @ workspace-section-route.route-entry.ts:12
(anonymous) @ default-section.element.ts:85
resolvePageComponent @ router.ts:165
await in resolvePageComponent
renderPath @ router-slot.ts:441
render @ router-slot.ts:257
(anonymous) @ router-slot.ts:170
requestAnimationFrame
connectedCallback @ router-slot.ts:168
O @ lit-html.js:6
T @ lit-html.js:6
$ @ lit-html.js:6
_$AI @ lit-html.js:6
(anonymous) @ lit-html.js:6
update @ lit-element.js:6
performUpdate @ reactive-element.js:6
scheduleUpdate @ reactive-element.js:6
_$EP @ reactive-element.js:6
await in _$EP
requestUpdate @ reactive-element.js:6
_$Ev @ reactive-element.js:6
constructor @ reactive-element.js:6
constructor @ lit-element.js:6
(anonymous) @ controller-host.mixin.ts:19
(anonymous) @ controller-host-element.mixin.ts:13
(anonymous) @ element.mixin.ts:13
constructor @ lit-element.element.ts:10
constructor @ router-slot.element.ts:83
u @ lit-html.js:6
$ @ lit-html.js:6
_$AI @ lit-html.js:6
p @ lit-html.js:6
$ @ lit-html.js:6
_$AI @ lit-html.js:6
(anonymous) @ lit-html.js:6
update @ lit-element.js:6
performUpdate @ reactive-element.js:6
scheduleUpdate @ reactive-element.js:6
_$EP @ reactive-element.js:6
await in _$EP
requestUpdate @ reactive-element.js:6
set @ reactive-element.js:6
(anonymous) @ default-section.element.ts:98
(anonymous) @ default-section.element.ts:91
await in (anonymous)
(anonymous) @ base-extensions-initializer.controller.ts:186
requestAnimationFrame
(anonymous) @ base-extensions-initializer.controller.ts:146
(anonymous) @ base-extension-initializer.controller.ts:329
await in (anonymous)
#gotManifest @ base-extension-initializer.controller.ts:169
(anonymous) @ base-extension-initializer.controller.ts:83
(anonymous) @ Subscriber.js:96
(anonymous) @ Subscriber.js:63
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ switchMap.js:14
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ innerFrom.js:51
(anonymous) @ Observable.js:38
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ switchMap.js:14
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ BehaviorSubject.js:19
(anonymous) @ Observable.js:38
(anonymous) @ Subject.js:95
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ Observable.js:66
(anonymous) @ Observable.js:30
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ switchMap.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
constructor @ observer.ts:16
constructor @ observer.controller.ts:19
observe @ class.mixin.ts:70
_init @ base-extension-initializer.controller.ts:72
constructor @ extension-manifest-initializer.controller.ts:26
_createController @ extensions-manifest-initializer.controller.ts:48
(anonymous) @ base-extensions-initializer.controller.ts:120
(anonymous) @ base-extensions-initializer.controller.ts:117
(anonymous) @ Subscriber.js:96
(anonymous) @ Subscriber.js:63
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ combineLatest.js:47
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ distinctUntilChanged.js:15
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ map.js:7
(anonymous) @ OperatorSubscriber.js:15
(anonymous) @ Subscriber.js:34
(anonymous) @ BehaviorSubject.js:19
(anonymous) @ Observable.js:38
(anonymous) @ Subject.js:95
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ Observable.js:66
(anonymous) @ Observable.js:30
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ combineLatest.js:40
maybeSchedule @ combineLatest.js:67
(anonymous) @ combineLatest.js:37
(anonymous) @ combineLatest.js:57
maybeSchedule @ combineLatest.js:67
(anonymous) @ combineLatest.js:31
(anonymous) @ Observable.js:38
(anonymous) @ Observable.js:32
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ map.js:6
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
(anonymous) @ distinctUntilChanged.js:10
(anonymous) @ lift.js:10
(anonymous) @ Observable.js:27
errorContext @ errorContext.js:19
(anonymous) @ Observable.js:23
constructor @ observer.ts:16
constructor @ observer.controller.ts:19
observe @ class.mixin.ts:70
_init @ base-extensions-initializer.controller.ts:86
constructor @ extensions-manifest-initializer.controller.ts:44
(anonymous) @ default-section.element.ts:57
constructor @ default-section.element.ts:48
createExtensionElement @ create-extension-element.function.ts:32
component @ backoffice-main.element.js:37
resolvePageComponent @ router.ts:136
renderPath @ router-slot.ts:441
render @ router-slot.ts:257
dispatchRouteChangeEvent @ events.ts:10
(anonymous) @ router-slot.ts:317
requestAnimationFrame
notifyChildRouters @ router-slot.ts:316
renderPath @ router-slot.ts:472
render @ router-slot.ts:257
dispatchGlobalRouterEvent @ events.ts:19
(anonymous) @ history.ts:64
#onSectionClick @ backoffice-header-sections.element.js:68
(anonymous) @ backoffice-header-sections.element.js:90
handleEvent @ lit-html.js:6
router-slot.ts:258 Uncaught (in promise) Context could not be found. (Context Alias: UmbPropertyContext with API Alias: UmbClipboardPropertyContext). Controller is hosted on #document-fragment > UMB-PROPERTY-EDITOR-UI-BLOCK-LIST

and if I click it I would expect the add Block panel to show. but I get an other error

router-slot.ts:258 Uncaught (in promise) Context could not be found. (Context Alias: UmbPropertyContext with API Alias: UmbClipboardPropertyContext). Controller is hosted on #document-fragment > UMB-PROPERTY-EDITOR-UI-BLOCK-LIST

For the record block config is

BlockList Config: 

(2) [{…}, {…}]

0: 

alias: "blocks"

value: Array(1)

0: 

backgroundColor: "#0c5f4a"

contentElementTypeKey: "266221db-e0ed-40c3-83e1-129e3a6bd9b5"

label: "Calendar Group"

settingsElementTypeKey: "87b4feb3-5f76-4ade-a172-6509505dadf0"

[[Prototype]]: Object

length: 1

[[Prototype]]: Array(0)

[[Prototype]]: Object

1: 

alias: "maxPropertyWidth"

value: "600px"

[[Prototype]]: Object

length: 2

[[Prototype]]: Array(0)

hey hello @DanielRogers-Intelli

thanks for sharing detailed code, i think the .config prop expects a UmbPropertyEditorConfigCollection instance (which has the getValueByAlias() method on it), not a plain array. Wrap your raw values before passing it in:

ts

import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
this.config = new UmbPropertyEditorConfigCollection(rawValues);

The UmbPropertyContext / UmbClipboardPropertyContext errors are because those contexts are normally provided by the property editor host further up the DOM they don’t exist in a custom workspace panel. The “Add button only appearing after visiting a content page” symptom is the same root cause; it’s picking up leaked context from the editor you just left.

The officially supported way to embed a property editor outside of content editing is using <umb-property-dataset> wrapping an <umb-property> — the dataset component provides all the required context for you:

html

<umb-property-dataset .value=${this.data} @change=${this.#onDataChange}>
    <umb-property
        label="Calendar Group"
        alias="calendarGroupBlocks"
        property-editor-ui-alias="Umb.PropertyEditorUi.BlockList"
        .config=${this.rawConfig}>
    </umb-property>
</umb-property-dataset>

See the docs here: https://docs.umbraco.com/umbraco-cms/customizing/property-editors/integrate-property-editors

i think theres also a github for example code umbraco Core i used to check with it and do it so pls have a check with that

Hi Bishal thanks for that. This has got me a long way further. I have now got a blocklist editor where I can add blocks/edit blocks/deleteblocks.

Note: I are not saving anything tey. So everytime I loaded it its an empty array.

However I still have a few errors and cant get a value for the array of blocks.

import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element"; import { html, css } from '@umbraco-cms/backoffice/external/lit'; import { umbHttpClient } from "@umbraco-cms/backoffice/http-client"; import '@umbraco-cms/backoffice/property-editor'; import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';

export class GenesisCalendarGroupsWorkspace extends UmbElementMixin(UmbLitElement) {
static properties = { blockListConfig: { type: Object }, config: {type: Object }, value: { type: Object } };

private data = [ { alias: "calendarGroupBlocks", value: { layout: [], contentData: [], settingsData: [] } } ];

config: any;
value: any | null = [];

connectedCallback() {
super.connectedCallback();
this.loadBlockListConfig();
}

`private async loadBlockListConfig() {
const response = await umbHttpClient.get({
url: ‘/umbraco/management/api/v1/data-type/55e53032-de54-49ae-9232-c79fcfa91c4e’,
security: [{ scheme: ‘bearer’, type: ‘http’ }],
});

const data = response.data as { values?: DataTypePropertyValue[] };

this.config = new UmbPropertyEditorConfigCollection(
    (data.values ?? []).map((item) => ({
        alias: item.alias,
        value: item.value,
    }))
);

this.requestUpdate();

}`

private onValueChange = (e: CustomEvent) => {
console.log('Property Dataset Changed', e.detail);
};

`` render() {
if (!this.config) return html<p>Loading...</p>;

    return html`
        <umb-property-dataset
            .value=${this.data} 
            @change=${this.onValueChange}>
          <umb-property
            alias="calendarGroupBlocks"
            label="Calendar Group"
            property-editor-ui-alias="Umb.PropertyEditorUi.BlockList"
            .config=${this.config}>
          </umb-property>
        </umb-property-dataset>
      `;
} ``

}
customElements.define("genesis-calendar-groups-workspace", GenesisCalendarGroupsWorkspace);

onValueChange always returns indefined.

if I change it to

private onValueChange = (e: any) => {
console.log('Property Dataset Changed', e);
};

I do get a result but there is nothing in it that seems useful. The key is @changechange is triggering but returning the wrong information.

The second obeservation is: On displaying the to the workspace I get x3 stops at this code (this.#promiseRejecter(
`Context request was cancelled, host was disconnected. (Context Alias: ${this.#contextAlias} with API Alias: ${this.#apiAlias}). Controller is hosted on ${hostElement?.parentNode?.nodeName ?? ‘Not attached node’} > ${hostElement?.nodeName}`,
);)

`` #handleDisconnect() {
	this.#disconnectAC = undefined;
	this.#unprovide();
	if (this.#promiseRejecter) {
		const hostElement = this._retrieveHost();
		this.#promiseRejecter(
			`Context request was cancelled, host was disconnected. (Context Alias: ${this.#contextAlias} with API Alias: ${this.#apiAlias}). Controller is hosted on ${hostElement?.parentNode?.nodeName ?? 'Not attached node'} > ${hostElement?.nodeName}`,
		);
	}
	this.#promise = undefined;
	this.#promiseOptions = undefined;
	this.#promiseResolver = undefined;
	this.#promiseRejecter = undefined;
} ``

After this I can add /edt/delete blocks

Third Observation: is I get this error

genesisCalendar.configuration.groups:1 Uncaught (in promise) Context request was cancelled, host was disconnected. (Context Alias: UmbPropertyDatasetContext with API Alias: default). Controller is hosted on Not attached node > UMB-PROPERTY-ACTION

I have tried adding

import { UMB_PROPERTY_CONTEXT } from "@umbraco-cms/backoffice/property"; import { UmbPropertyContext } from "@umbraco-cms/backoffice/property";
….
private propertyContext = new UmbPropertyContext(this);

` connectedCallback() {
super.connectedCallback();

    this.provideContext(UMB_PROPERTY_CONTEXT, this.propertyContext);

    // Wait for first render so the workspace shell exists
    this.loadBlockListConfig();
}`

`` render() {
if (!this.config) return html<p>Loading...</p>;

    return html`
        <umb-property-dataset
            .value=${this.data} 
            .propertyContext=${this.propertyContext}
            @change=${this.onValueChange}>
          <umb-property
            alias="calendarGroupBlocks"
            label="Calendar Group"
            property-editor-ui-alias="Umb.PropertyEditorUi.BlockList"
            .config=${this.config}>
          </umb-property>
        </umb-property-dataset>
      `;
} ``

But didnt have any obvious results.

I have also tried gettin@change the @change off of umb-property as expected never triggers anything.

For contect this is the manifest snipit that relates to the worspace

const configurationGroupsMenuItem: ManifestMenuItem = { type: 'menuItem', alias: 'GenesisCalendar.Menu.Configuration.Groups', name: 'Groups', meta: { label: 'Groups', icon: 'icon-thumbnails-small', menus: ['genesis-calendar.Menu'], entityType: 'genesisCalendar.configuration.groups' } };

const configurationGroupsWorkspace: ManifestWorkspace = { type: 'workspace', alias: 'GenesisCalendar.Workspace.Configuration.Groups', name: 'Calendar Groups Workspace', elementName: 'genesis-calendar-groups-workspace', meta: { entityType: 'genesisCalendar.configuration.groups' } };

I have go a long way to getting this working but are still battling a last step I think. The db has one record that wont dispplay on render. but the error Im getting seems to be related to mounting the UmbClipboardPropertyContext.

the goal is to use the blocklist inside a custom section workspace. I have really hit a brick wall here and been chacing this round in circles for a couple of days.

manifest is decleared**
**
`// Main Configuration Category Header Node Folder
const configurationMenuItem: ManifestMenuItem = {
type: ‘menuItem’,
alias: ‘GenesisCalendar.Menu.Configuration’,
name: ‘Configuration’,
meta: {
label: ‘Configuration’,
icon: ‘icon-settings’,
menus: [‘genesis-calendar.Menu’],
entityType: ‘genesisCalendar.configuration’
}
};

// NESTED CHILD GROUPS LINK
const configurationGroupsMenuItem: ManifestMenuItem = {
type: ‘menuItem’,
alias: ‘GenesisCalendar.Menu.Configuration.Groups’,
name: ‘Groups’,
meta: {
label: ‘Groups’,
icon: ‘icon-thumbnails-small’,
// Target your core master sidebar app collection instance identifier name
menus: [‘genesis-calendar.Menu’],
// FIX: Nest underneath your custom Configuration folder node cleanly
parentUnique: ‘GenesisCalendar.Menu.Configuration’,
entityType: ‘genesisCalendar.configuration.groups’,
pathname: ‘groups’
}
};`

// GROUPS PAGE SHELL (Points directly to your split compiled output file) const configurationGroupsWorkspace: ManifestWorkspace = { type: 'workspace', alias: 'GenesisCalendar.Workspace.Configuration.Groups', // 👈 ROUTING TARGET name: 'Calendar Groups Workspace Page', elementName: 'genesis-calendar-groups-workspace', element: () => import('../backoffice/groups/UUI-Groups.Panel.ts'), meta: { entityType: 'genesisCalendar.configuration.groups' } };

the UUI-Groups.Panel.ts

import { UmbLitElement } from “@umbraco-cms/backoffice/lit-element”;
import { html, css } from “@umbraco-cms/backoffice/external/lit”;
import { umbHttpClient } from “@umbraco-cms/backoffice/http-client”;
import { UmbElementMixin } from “@umbraco-cms/backoffice/element-api”;
import { UmbPropertyContext } from “@umbraco-cms/backoffice/property”;
import { UmbPropertyEditorConfigCollection } from “@umbraco-cms/backoffice/property-editor”;
import { UmbContextBase } from “@umbraco-cms/backoffice/class-api”;
import { UmbObjectState } from “@umbraco-cms/backoffice/observable-api”;
import { UmbContextProviderController, UmbContextToken } from “@umbraco-cms/backoffice/context-api”;
import type { UmbControllerHost } from “@umbraco-cms/backoffice/controller-api”;
import {
UMB_WORKSPACE_CONTEXT,
UMB_VARIANT_WORKSPACE_CONTEXT,
type UmbWorkspaceContext,
type UmbVariantDatasetWorkspaceContext
} from “@umbraco-cms/backoffice/workspace”;

/* -------------------------------------------------------------
TYPES & SCHEMAS
------------------------------------------------------------- */
interface BlockEditorModel {
layout: Array<{ contentKey: string; settingsKey: string }>;
contentData: any
;
settingsData: any
;
}

interface BlockEditorModel {
layout: Array<{ contentKey: string; settingsKey: string }>;
contentData: any
;
settingsData: any
;
}

interface BlockListElementModel {
key: string;
[alias: string]: unknown;
}

interface BlockListItem {
contentKey: string;
settingsKey: string;
content: BlockListElementModel;
settings: BlockListElementModel;
}

/* -------------------------------------------------------------
Calendar group model (DB row)
------------------------------------------------------------- */
interface CalendarGroup {
key: string;
name: string;
blockListJson: BlockListItem | null;
}

type BlockListDatatypeConfig = Array<{ alias: string; value: any }>;

const UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_TOKEN = new UmbContextToken(
“UmbDocumentTypeRepository”,
“UmbDocumentTypeDetailRepository”
);

const UMB_DOCUMENT_TYPE_REFERENCE_REPOSITORY_TOKEN = new UmbContextToken(
“UmbDocumentTypeRepository”,
“UmbDocumentTypeReferenceRepository”
);

const UMB_DATA_TYPE_DETAIL_REPOSITORY_TOKEN = new UmbContextToken(
“UmbDataTypeRepository”,
“UmbDataTypeDetailRepository”
);

const UMB_DATA_TYPE_REFERENCE_REPOSITORY_TOKEN = new UmbContextToken(
“UmbDataTypeRepository”,
“UmbDataTypeReferenceRepository”
);

const UMB_PROPERTY_DATASET_CONTEXT_TOKEN = new UmbContextToken(
“UmbPropertyDatasetContext”
);

const STABLE_WORKSPACE_UNIQUE_KEY = “genesis-calendar-groups-static-node”;

/* -------------------------------------------------------------
Helpers
------------------------------------------------------------- */

function deriveNameFromBlock(block: BlockListItem | null | undefined): string {
if (!block || !block.content) return “Untitled”;

const title = block.content["title"];
return typeof title === "string" && title.trim().length > 0
    ? title
    : "Untitled";

}

/* -------------------------------------------------------------
CORE LAYOUT MOCK SUB-CONTEXTS (Class structures with RxJS Streams)
------------------------------------------------------------- */
class MockWorkspaceContext extends UmbContextBase implements UmbWorkspaceContext {
public api = “default”;
public workspaceAlias = “default”;

// Real observable streams to satisfy internal combineLatest checks
public readonly name = new UmbObjectState<string>("Calendar Groups Workspace").asObservable();
public readonly unique = new UmbObjectState<string>(STABLE_WORKSPACE_UNIQUE_KEY).asObservable();
public readonly entityType = new UmbObjectState<string>("document").asObservable();
public readonly readByCulture = new UmbObjectState<boolean>(false).asObservable();
public readonly variantInfo = new UmbObjectState<any>(void 0).asObservable();

constructor(host: any) {
    super(host, UMB_WORKSPACE_CONTEXT);
}

public getWorkspaceAlias() { return "default"; }
public getEntityType() { return "document"; }
public getIsNew() { return false; }
public getUnique() { return STABLE_WORKSPACE_UNIQUE_KEY; } // ⭐ FIX: Returns static string key reference
public isVariantDirty() { return false; }
public getVariantInfo() { return void 0; }

}

class MockVariantWorkspaceContext extends UmbContextBase {
public api = “default”;
public workspaceAlias = “default”;

public readonly name = new UmbObjectState<string>("Calendar Groups Workspace").asObservable();
public readonly unique = new UmbObjectState<string>(STABLE_WORKSPACE_UNIQUE_KEY).asObservable();
public readonly entityType = new UmbObjectState<string>("document").asObservable();
public readonly readByCulture = new UmbObjectState<boolean>(false).asObservable();
public readonly variantInfo = new UmbObjectState<any>(void 0).asObservable();
public readonly culture = new UmbObjectState<any>(void 0).asObservable();
public readonly segment = new UmbObjectState<any>(void 0).asObservable();
public currentVariant = { culture: void 0, segment: void 0 };

constructor(host: any) {
    super(host, UMB_VARIANT_WORKSPACE_CONTEXT);
}

public getWorkspaceAlias() { return "default"; }
public getEntityType() { return "document"; }
public getActiveVariantInfo() { return void 0; }
public getName() { return "Calendar Groups Workspace"; }
public getUnique() { return STABLE_WORKSPACE_UNIQUE_KEY; }
public getIsNew() { return false; }

}

class MockDocumentTypeRepository extends UmbContextBase {
public api = “UmbDocumentTypeRepository”;

constructor(host: any) {
    // Pass our manually generated token down to the context tracking layer base class
    super(host, UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_TOKEN);
}

public requestByUniques(uniques: string[]) {
    const mockSchemas = uniques.map((unique) => {
        if (unique === "266221db-e0ed-40c3-83e1-129e3a6bd9b5") {
            return {
                unique: "266221db-e0ed-40c3-83e1-129e3a6bd9b5",
                entityType: "document-type",
                name: "Calendar Group",
                alias: "IBD_GS_CalendarGroup",
                icon: "icon-plugin",
                isElement: true,
                containers: [
                    { unique: "7bb24f46-8c34-4382-b186-24b579cb96d9", name: "Content", alias: "content", type: "Tab", sortOrder: 0 },
                    { unique: "5928183c-06bf-48c3-ad84-07f78ccd931b", name: "Sumary", alias: "sumary", type: "Tab", sortOrder: 1 }
                ],
                properties: [
                    { unique: "2fd0a29a-cd85-4cac-97d2-934646808f53", name: "Title", alias: "title", propertyEditorUiAlias: "Umb.PropertyEditorUi.TextBox", containerKey: "7bb24f46-8c34-4382-b186-24b579cb96d9", sortOrder: 0, appearance: { labelOnTop: true } },
                    { unique: "4ed29681-81a6-40e4-870d-5e903faee7bd", name: "Info", alias: "info", propertyEditorUiAlias: "Umb.PropertyEditorUi.TinyMCE", containerKey: "7bb24f46-8c34-4382-b186-24b579cb96d9", sortOrder: 1, appearance: { labelOnTop: true } },
                    { unique: "7c075aef-fa77-4bd4-95de-c09864616985", name: "Sumary", alias: "sumary", propertyEditorUiAlias: "Umb.PropertyEditorUi.TinyMCE", containerKey: "5928183c-06bf-48c3-ad84-07f78ccd931b", sortOrder: 0, appearance: { labelOnTop: true } }
                ]
            };
        }
        if (unique === "87b4feb3-5f76-4ade-a172-6509505dadf0") {
            return {
                unique: "87b4feb3-5f76-4ade-a172-6509505dadf0",
                entityType: "document-type",
                name: "Calendar Group (Settings)",
                alias: "IBD_GSS_CalendarGroup",
                icon: "icon-plugin",
                isElement: true,
                containers: [{ unique: "77ab0718-af6b-40ba-9387-77f653c96619", name: "Component", alias: "component", type: "Tab", sortOrder: 1 }],
                properties: [{ unique: "ee8d0a79-cece-4e79-9e77-7a63ea8b2ed1", name: "Style", alias: "style", propertyEditorUiAlias: "Genesis.Theme.Picker", containerKey: "77ab0718-af6b-40ba-9387-77f653c96619", sortOrder: 0, appearance: { labelOnTop: true } }]
            };
        }
        // This ensures that when #initializeStructures crawls the map, it resolves natively.
        if (unique === "55e53032-de54-49ae-9232-c79fcfa91c4e") {
            return {
                unique: "55e53032-de54-49ae-9232-c79fcfa91c4e",
                entityType: "data-type",
                name: "Calendar Groups Block List",
                propertyEditorAlias: "Umbraco.BlockList",
                values: []
            };
        }
        return { unique, entityType: "document-type", name: "Mock", alias: "mock", isElement: true, containers: [], properties: [] };
    });
    return Promise.resolve({ data: mockSchemas });
}
public readMany(uniques: string[]) { return this.requestByUniques(uniques); }

}

class MockClipboardPropertyContext extends UmbContextBase {
public api = “UmbClipboardPropertyContext”;

// Provide the observable streams expected by the block layout manager
public readonly entries = new UmbObjectState<any[]>([]).asObservable();
public readonly allowedTypes = new UmbObjectState<any[]>([]).asObservable();

constructor(host: any) {
    // Pass the host and the fallback lookup string key down to the base class tracker
    super(host, "UmbClipboardPropertyContext");
}

public copy() { /* no-op */ }
public paste() { return Promise.resolve(null); }
public clear() { /* no-op */ }

}

class MockPropertyDatasetContext extends UmbContextBase {
private _valueState = new UmbObjectState(undefined);
public readonly value = this._valueState.asObservable();

constructor(host: any) {
    super(host, "UmbPropertyDatasetContext");
}

public setPropertyValue(alias: string, value: any) {
    this._valueState.setValue(value);
}

public propertyValueByAlias(alias: string) {
    return this.value;
}

public getPropertyValue(alias: string) {
    return this._valueState.getValue();
}

}

/* -------------------------------------------------------------
MAIN VISUAL PANELS COMPONENT
------------------------------------------------------------- */

export class GenesisCalendarGroupsWorkspace extends UmbElementMixin(UmbLitElement) {
static properties = {
groups: { type: Array },
config: { type: Object },
value: { type: Object },
blockListConfig: { type: Object },
isMounted: { type: Boolean, state: true }
};

public config: UmbPropertyEditorConfigCollection | null = null;
public value: BlockEditorModel | null = null;
public groups: CalendarGroup[] = [];
public blockListConfig: BlockListDatatypeConfig | null = null;

private isMounted = false;

private propertyContext?: UmbPropertyContext;
private datasetContext: MockPropertyDatasetContext;
private _customContextsMap?: Map<string, any>;

get _umbContexts(): Map<string, any> {
    if (!this._customContextsMap) {
        this._customContextsMap = new Map();

        const workspace = new MockWorkspaceContext(this);
        const variant = new MockVariantWorkspaceContext(this);
        const repo = new MockDocumentTypeRepository(this);
        const clipboard = new MockClipboardPropertyContext(this);

        this._customContextsMap.set("UmbWorkspaceContext", workspace);
        this._customContextsMap.set("UmbWorkspaceContext:default", workspace);
        this._customContextsMap.set("UmbVariantWorkspaceContext", variant);
        this._customContextsMap.set("UmbVariantWorkspaceContext:default", variant);
        this._customContextsMap.set("UmbClipboardPropertyContext", clipboard);
        this._customContextsMap.set("UmbPropertyContext:UmbClipboardPropertyContext", clipboard);
        this._customContextsMap.set("UmbDocumentTypeRepository", repo);
        this._customContextsMap.set("UmbDocumentTypeDetailRepository", repo);
        this._customContextsMap.set("UmbDocumentRepository", repo);
        this._customContextsMap.set("UmbDocumentDetailRepository", repo);
    }
    return this._customContextsMap;
}

set _umbContexts(val: Map<string, any>) {
    if (val && this._customContextsMap) {
        for (const [k, v] of val.entries()) {
            this._customContextsMap.set(k, v);
        }
    }
}

constructor() {
    super();

    this.value = { layout: [], contentData: [], settingsData: [] };
    this.isMounted = false;

    this.propertyContext = new UmbPropertyContext(this);
    this.propertyContext.setAlias("calendarGroupBlocks");

    this.datasetContext = new MockPropertyDatasetContext(this);

    // Initialize context map NOW, not lazily
    this._customContextsMap = new Map();

    const workspace = new MockWorkspaceContext(this);
    const variant = new MockVariantWorkspaceContext(this);
    const repo = new MockDocumentTypeRepository(this);
    const clipboard = new MockClipboardPropertyContext(this);

    this._customContextsMap.set("UmbWorkspaceContext", workspace);
    this._customContextsMap.set("UmbWorkspaceContext:default", workspace);
    this._customContextsMap.set("UmbVariantWorkspaceContext", variant);
    this._customContextsMap.set("UmbVariantWorkspaceContext:default", variant);
    this._customContextsMap.set("UmbClipboardPropertyContext", clipboard);
    this._customContextsMap.set("UmbPropertyContext:UmbClipboardPropertyContext", clipboard);
    this._customContextsMap.set("UmbDocumentTypeRepository", repo);
    this._customContextsMap.set("UmbDocumentTypeDetailRepository", repo);
    this._customContextsMap.set("UmbDocumentRepository", repo);
    this._customContextsMap.set("UmbDocumentDetailRepository", repo);
}

async connectedCallback() {
    super.connectedCallback();

    const workspace = this._umbContexts.get("UmbWorkspaceContext");
    const variant = this._umbContexts.get("UmbVariantWorkspaceContext");
    const repo = this._umbContexts.get("UmbDocumentTypeRepository");
    const clipboardMock = this._umbContexts.get("UmbClipboardPropertyContext");

    // Workspace + variant (tokens + legacy string aliases)
    new UmbContextProviderController(this, UMB_WORKSPACE_CONTEXT, workspace as unknown as UmbWorkspaceContext);
    new UmbContextProviderController(this, UMB_VARIANT_WORKSPACE_CONTEXT, variant as unknown as UmbVariantDatasetWorkspaceContext<any>);
    new UmbContextProviderController(this, UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_TOKEN, repo as any);
    new UmbContextProviderController(this, UMB_DOCUMENT_TYPE_REFERENCE_REPOSITORY_TOKEN, repo as any);
    new UmbContextProviderController(this, UMB_DATA_TYPE_DETAIL_REPOSITORY_TOKEN, repo as any);
    new UmbContextProviderController(this, UMB_DATA_TYPE_REFERENCE_REPOSITORY_TOKEN, repo as any);

    // Repositories
    new UmbContextProviderController(this, "UmbPropertyContext:UmbClipboardPropertyContext", clipboardMock);
    new UmbContextProviderController(this, UMB_PROPERTY_DATASET_CONTEXT_TOKEN, this.datasetContext);
    new UmbContextProviderController(this, "UmbPropertyDatasetContext", this.datasetContext);

    // REGISTER THE CLIPBOARD CONTEXT ON THE GLOBAL LAYOUT FOOTPRINT
    // This guarantees that detached floating popovers (<umb-popover-layout>) 
    // can crawl down and locate your clipboard values successfully!
    const globalTargetHost = document.querySelector("umb-backoffice") as UmbControllerHost | null;

    if (globalTargetHost) {
        new UmbContextProviderController(
            globalTargetHost,
            "UmbPropertyContext:UmbClipboardPropertyContext",
            clipboardMock
        );

        const globalDatasetProxyFallback = {
            setPropertyValue: (alias: string, val: any) => {
                this.datasetContext.setPropertyValue(alias, val);
                this.propertyContext?.setValue(val);
            },
            propertyValueByAlias: (alias: string) => this.datasetContext.propertyValueByAlias(alias),
            getPropertyValue: (alias: string) => this.datasetContext.getPropertyValue(alias)
        };

        new UmbContextProviderController(
            globalTargetHost,
            "UmbPropertyDatasetContext",
            globalDatasetProxyFallback as any
        );
    }


    await this.loadBlockListConfig();
    await this.loadGroups();

    this.isMounted = true;
    this.requestUpdate();

    this.observe(this.datasetContext.propertyValueByAlias("calendarGroupBlocks"), (newValue: any) => {
        if (newValue) {
            this.value = newValue;
            this.requestUpdate();
        }
    });
}


/* ---------------------------------------------------------
   Load Block List datatype config
   --------------------------------------------------------- */

private async loadBlockListConfig() {
    const dataTypeId = "55e53032-de54-49ae-9232-c79fcfa91c4e"; 
    try {
        const response = await umbHttpClient.get({
            url: `/umbraco/management/api/v1/data-type/${dataTypeId}`,
            security: [{ scheme: "bearer", type: "http" }]
        });
                    
        const data: any = response.data;
        if (data && data.values) {
            this.blockListConfig = data.values;

            // 3. Construct the config collection and feed it to the context
            const configCollection = new UmbPropertyEditorConfigCollection(data.values);
            this.config = configCollection; 
            this.propertyContext?.setConfig(configCollection);
        }
    } catch(error) {
        console.error("Failed to load block list configuration:", error);
    }
}

/* ---------------------------------------------------------
   Load all groups from DB
   --------------------------------------------------------- */
private async loadGroups() {
    try {
        const response = await umbHttpClient.get({
            url: "/umbraco/management/api/v1/genesis/calendar/groups",
            security: [{ scheme: "bearer", type: "http" }]
        });

        this.groups = response.data as CalendarGroup[];
        console.log("raw groups: ", this.groups);

        const aggregatedModel: BlockEditorModel = {
            layout: [],
            contentData: [],
            settingsData: []
        };

        for (const group of this.groups) {
            if (group.blockListJson) {

                // ⭐ FIX: Parse JSON string into object
                const block: BlockListItem =
                    typeof group.blockListJson === "string"
                        ? JSON.parse(group.blockListJson)
                        : group.blockListJson;

                aggregatedModel.layout.push({ contentKey: block.contentKey, settingsKey: block.settingsKey });
                aggregatedModel.contentData.push(block.content);
                aggregatedModel.settingsData.push(block.settings);
            }
        }

        console.log("aggregatedModel: ", aggregatedModel);

        // ⭐ Store value and push into context
        this.value = aggregatedModel;
        this.propertyContext?.setValue(aggregatedModel);

    } catch (error) {
        console.error("Failed to load calendar groups:", error);
    }
}


/* ---------------------------------------------------------
   BlockList change → update groups
   --------------------------------------------------------- */

private blockEditorToGroups(model: BlockEditorModel | null): CalendarGroup[] {
    if (!model || !model.layout) return [];

    return model.layout.map((layoutItem: any) => {
        const content = model.contentData.find((x: any) => x.key === layoutItem.contentKey);
        const settings = model.settingsData.find((x: any) => x.key === layoutItem.settingsKey);

        const block: BlockListItem = {
            contentKey: layoutItem.contentKey,
            settingsKey: layoutItem.settingsKey,
            content,
            settings
        };

        return {
            key: content.key,
            name: deriveNameFromBlock(block),
            blockListJson: block
        };
    });
}

private onBlockListChange(e: any) {
    // 1. Cast the target to retrieve the updated block list data model
    const target = e.target as any; 
    const updatedBlockEditorModel = target.value as BlockEditorModel;

    console.log("EDITOR MODEL:", updatedBlockEditorModel);

    if (!updatedBlockEditorModel) return;

    // 2. Map the structural layout nodes back into your granular CalendarGroup DB records
    this.groups = updatedBlockEditorModel.layout.map(layoutItem => {
        const content = updatedBlockEditorModel.contentData.find(x => x.key === layoutItem.contentKey);
        const settings = updatedBlockEditorModel.settingsData.find(x => x.key === layoutItem.settingsKey);

        const blockItem: BlockListItem = {
            contentKey: layoutItem.contentKey,
            settingsKey: layoutItem.settingsKey,
            content: content,
            settings: settings
        };

        return {
            key: content?.key || crypto.randomUUID(), // Match or assign identifier
            name: deriveNameFromBlock(blockItem),
            blockListJson: blockItem
        };
    });

    // 3. Keep your active property context updated to reflect the new state tree
    this.propertyContext?.setValue(updatedBlockEditorModel);
}

/* ---------------------------------------------------------
   SAVE ALL GROUPS
   --------------------------------------------------------- */

private async onSaveClick() {
    if (!this.value) {
        console.warn("No Block Editor value to save.");
        return;
    }

    const groups = this.blockEditorToGroups(this.value);

    // Upsert
    for (const group of groups) {
        const exists = this.groups.find((g) => g.key === group.key);

        if (exists) {
            await this.saveGroup(group);
        } else {
            await this.createGroup(group);
        }
    }

    // Delete removed
    for (const old of this.groups) {
        if (!groups.find((g) => g.key === old.key)) {
            await this.deleteGroup(old.key);
        }
    }

    console.log("All groups saved.");

    // Reload from DB and push new model into hydrated editor
    await this.loadGroups();
}

private async createGroup(group: CalendarGroup) {
    await umbHttpClient.post({
        url: `/umbraco/management/api/v1/genesis/calendar/groups`,
        body: {
            key: group.key,
            name: group.name,
            blockListJson: JSON.stringify(group.blockListJson)
        },
        security: [{ scheme: "bearer", type: "http" }]
    });
}

private async saveGroup(group: CalendarGroup): Promise<void> {
    try {
        await umbHttpClient.put({
            url: `/umbraco/management/api/v1/genesis/calendar/groups/${group.key}`,
            body: {
                key: group.key,
                name: group.name,
                blockListJson: JSON.stringify(group.blockListJson)
            },
            security: [{ scheme: "bearer", type: "http" }]
        });
    } catch (err) {
        console.error("Failed to save group:", err);
    }
}

private async deleteGroup(key: string) {
    await umbHttpClient.delete({
        url: `/umbraco/management/api/v1/genesis/calendar/groups/${key}`,
        security: [{ scheme: "bearer", type: "http" }]
    });
}

/* ---------------------------------------------------------
   Render (STATIC host + footer)
   --------------------------------------------------------- */
render() {
    const ready = this.isMounted && this.blockListConfig && this.propertyContext;

    // Fall back to a default block editor model schema if the context state is empty
    const value = this.propertyContext?.getValue();

    console.log("context:", this.propertyContext);
    console.log("Active Block value:", this.value); 

    return html`
        <div class="workspace-container" style="padding: 24px; display: flex; flex-direction: column; gap: 16px;">
            <h2>Genesis Calendar Groups</h2>

            ${ready
                    ? html`
                    <!-- 1. Provide the Property Dataset Context to the nested tree -->
                    <umb-context-provider key="UmbPropertyDatasetContext" .value=${this.datasetContext}>
                        <umb-property-dataset .value=${[{ alias: "calendarGroupBlocks", value: this.value }]}>
                            <umb-property
                                alias="calendarGroupBlocks"
                                hide-label
                                no-margin
                                property-editor-ui-alias="Umb.PropertyEditorUi.BlockList"
                                .config=${this.config}
                                .value=${this.value}
                                @property-value-change=${this.onBlockListChange}>
                            </umb-property>
                        </umb-property-dataset>
                    </umb-context-provider>
                `
                    : html`<umb-load-indicator></umb-load-indicator>`
                }

            <div class="footer" style="display: flex; justify-content: flex-end; margin-top: 16px;">
                <uui-button
                    look="primary"
                    color="positive"
                    label="Save groups"
                    @click=${this.onSaveClick}>
                    Save groups
                </uui-button>
            </div>
        </div>
    `;
}

static styles = css`
    :host {
      display: flex;
      flex-direction: column;
      height: 100%;
    }

    .main-content {
      flex: 1;
      overflow: auto;
      padding-bottom: 70px;
    }

    .footer {
      position: sticky;
      bottom: 0;
      background: var(--uui-color-surface);
      padding: 12px 20px;
      border-top: 1px solid var(--uui-color-border);
      display: flex;
      justify-content: flex-end;
      z-index: 1000;
    }
`;

}

customElements.define(
“genesis-calendar-groups-workspace”,
GenesisCalendarGroupsWorkspace as CustomElementConstructor
);

// Export the view element directly as default so your layout manifest maps it cleanly
export default GenesisCalendarGroupsWorkspace;

and the error is

genesisCalendar.configuration.groups:1 Uncaught (in promise) Context request was cancelled, host was disconnected. (Context Alias: UmbPropertyDatasetContext with API Alias: default). Controller is hosted on Not attached node > UMB-PROPERTY-ACTION

Hey @DanielRogers-Intelli

I was testing this exact architecture locally and ran into the exact same brick walls. You are incredibly close! I spent a few hours building this out from scratch—getting a bit of help from AI to untangle the Context API along the way—to figure out exactly what was going wrong.

The Not attached node and Context request was cancelled errors are happening because transient popover components (like <umb-property-action> and the Block List overlays) strictly request contexts using specific API discriminators. When they query your mock contexts and don’t find a matching api alias, the requests bubble up into the void, timeout, and crash when the node detaches.

To debug this, I set up a completely isolated, vanilla JavaScript proof-of-concept (bypassing any TypeScript/Vite build steps to eliminate variables). Here is the complete setup, the folder structure, and the exact code needed to get your workspace rendering, copying, and saving successfully.

Phase 1: The Setup & Folder Structure

I created a custom section at the root level called “Genesis” to ensure the standard Umbraco settings tree wasn’t interfering.

Here is the exact folder structure for the POC:

YourUmbracoProject/
│
├── Controllers/
│   └── GenesisCalendarGroupsController.cs
│
└── App_Plugins/
    └── GenesisCalendar/
        ├── umbraco-package.json
        └── genesis-calendar.js

Phase 2: Backoffice Manifest
The complete umbraco-package.json:

{
  "$schema": "../../umbraco-package-schema.json",
  "name": "Genesis Calendar",
  "version": "1.0.0",
  "extensions": [
    {
      "type": "section",
      "alias": "GenesisCalendar.Section",
      "name": "Genesis Calendar Section",
      "meta": {
        "label": "Genesis",
        "pathname": "genesis"
      }
    },
    {
      "type": "menu",
      "alias": "GenesisCalendar.Menu",
      "name": "Genesis Calendar Menu"
    },
    {
      "type": "sectionSidebarApp",
      "kind": "menuWithEntityActions",
      "alias": "GenesisCalendar.SectionSidebar",
      "name": "Genesis Calendar Sidebar",
      "conditions": [
        {
          "alias": "Umb.Condition.SectionAlias",
          "match": "GenesisCalendar.Section"
        }
      ],
      "meta": {
        "label": "Calendar Configurations",
        "menu": "GenesisCalendar.Menu"
      }
    },
    {
      "type": "menuItem",
      "alias": "GenesisCalendar.Menu.Configuration.Groups",
      "name": "Groups",
      "meta": {
        "label": "Groups",
        "icon": "icon-thumbnails-small",
        "menus": [ "GenesisCalendar.Menu" ],
        "entityType": "genesisCalendar.configuration.groups",
        "pathname": "calendar-groups"
      }
    },
    {
      "type": "workspace",
      "alias": "GenesisCalendar.Workspace.Configuration.Groups",
      "name": "Calendar Groups Workspace Page",
      "elementName": "genesis-calendar-groups-workspace",
      "js": "/App_Plugins/GenesisCalendar/genesis-calendar.js",
      "meta": {
        "entityType": "genesisCalendar.configuration.groups"
      }
    }
  ]
}

Phase 3: The Context API Fixes (Frontend)

Here are the core fixes applied to the JavaScript/TypeScript file:

  1. The Dataset Context: The token needs the "default" discriminator, and your MockPropertyDatasetContext (and global proxy) must explicitly expose api = "default".
  2. The Clipboard Context: Passing the clipboard context as a raw string fails in deep overlays. You must instantiate a strict UmbContextToken.
  3. The “Dueling Contexts” HTML Trap: Wrapping the UI in <umb-property-dataset> creates a conflicting internal context that swallows the data. I removed that HTML tag and forced the <umb-property> to talk directly to our mock dataset.
  4. The Copy/Paste Crash (t?.getName is not a function): The UI crashes on copy because the mock database repository returns a fake schema object without a getName() function. Fix: Delete the mock repository, create a real Element Type in the backoffice, and pass its real GUID to the config.
  5. V14+ Nested Layout Mapping: Mapping the layout to a flat array (layout: []) causes the UI to render blank on load. Modern Umbraco strictly requires layout: { "Umbraco.BlockList": [] }.

The complete genesis-calendar.js: (Note: I translated this to native JS for the POC, but the Context logic is identical for TS).

import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { html, css } from "@umbraco-cms/backoffice/external/lit";
import { umbHttpClient } from "@umbraco-cms/backoffice/http-client";
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
import { UmbPropertyContext } from "@umbraco-cms/backoffice/property";
import { UmbPropertyEditorConfigCollection } from "@umbraco-cms/backoffice/property-editor";
import { UmbContextBase } from "@umbraco-cms/backoffice/class-api";
import { UmbObjectState } from "@umbraco-cms/backoffice/observable-api";
import { UmbContextProviderController, UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import { UMB_WORKSPACE_CONTEXT, UMB_VARIANT_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/workspace";

// --- TOKENS ---
const UMB_PROPERTY_DATASET_CONTEXT_TOKEN = new UmbContextToken("UmbPropertyDatasetContext", "default");
const UMB_CLIPBOARD_PROPERTY_CONTEXT_TOKEN = new UmbContextToken("UmbPropertyContext", "UmbClipboardPropertyContext");
const STABLE_WORKSPACE_UNIQUE_KEY = "genesis-calendar-groups-static-node";

// --- MOCK CONTEXTS ---
class MockWorkspaceContext extends UmbContextBase {
    constructor(host) {
        super(host, UMB_WORKSPACE_CONTEXT);
        this.api = "default";
        this.workspaceAlias = "default";
        this.name = new UmbObjectState("Calendar Groups Workspace").asObservable();
        this.unique = new UmbObjectState(STABLE_WORKSPACE_UNIQUE_KEY).asObservable();
        this.entityType = new UmbObjectState("genesisCalendar.configuration.groups").asObservable();
        this.readByCulture = new UmbObjectState(false).asObservable();
        this.variantInfo = new UmbObjectState(undefined).asObservable();
    }
    getWorkspaceAlias() { return "default"; }
    getEntityType() { return "genesisCalendar.configuration.groups"; }
    getIsNew() { return false; }
    getUnique() { return STABLE_WORKSPACE_UNIQUE_KEY; }
    isVariantDirty() { return false; }
    getVariantInfo() { return undefined; }
    getName() { return "Calendar Groups Workspace"; }
}

class MockVariantWorkspaceContext extends UmbContextBase {
    constructor(host) {
        super(host, UMB_VARIANT_WORKSPACE_CONTEXT);
        this.api = "default";
        this.workspaceAlias = "default";
        this.name = new UmbObjectState("Calendar Groups Workspace").asObservable();
        this.unique = new UmbObjectState(STABLE_WORKSPACE_UNIQUE_KEY).asObservable();
        this.entityType = new UmbObjectState("genesisCalendar.configuration.groups").asObservable();
        this.readByCulture = new UmbObjectState(false).asObservable();
        this.variantInfo = new UmbObjectState(undefined).asObservable();
        this.culture = new UmbObjectState(undefined).asObservable();
        this.segment = new UmbObjectState(undefined).asObservable();
        this.currentVariant = { culture: undefined, segment: undefined };
    }
    getWorkspaceAlias() { return "default"; }
    getEntityType() { return "genesisCalendar.configuration.groups"; }
    getActiveVariantInfo() { return undefined; }
    getName() { return "Calendar Groups Workspace"; }
    getUnique() { return STABLE_WORKSPACE_UNIQUE_KEY; }
    getIsNew() { return false; }
}

class MockClipboardPropertyContext extends UmbContextBase {
    constructor(host) {
        super(host, "UmbClipboardPropertyContext");
        this.api = "UmbClipboardPropertyContext";
        this._entriesState = new UmbObjectState([]);
        this.entries = this._entriesState.asObservable();
        this.allowedTypes = new UmbObjectState([]).asObservable();
    }
    copy(item) {
        this._entriesState.setValue([item]);
    }
    paste() {
        const currentEntries = this._entriesState.getValue();
        return Promise.resolve(currentEntries.length > 0 ? currentEntries[0] : null);
    }
    clear() { this._entriesState.setValue([]); }
}

class MockPropertyDatasetContext extends UmbContextBase {
    constructor(host) {
        super(host, "UmbPropertyDatasetContext");
        this.api = "default";
        this._valueState = new UmbObjectState(undefined);
        this.value = this._valueState.asObservable();
    }
    setPropertyValue(alias, value) { this._valueState.setValue(value); }
    propertyValueByAlias(alias) { return this.value; }
    getPropertyValue(alias) { return this._valueState.getValue(); }
    getName() { return "Calendar Group Dataset"; }
    getUnique() { return "dataset-unique-id"; }
    getEntityType() { return "genesisCalendar.configuration.groups"; }
}

// --- MAIN RUNTIME ELEMENT ---
export class GenesisCalendarGroupsWorkspace extends UmbElementMixin(UmbLitElement) {
    static properties = {
        groups: { type: Array },
        config: { type: Object },
        value: { type: Object },
        blockListConfig: { type: Object },
        isMounted: { type: Boolean, state: true }
    };

    constructor() {
        super();
        this.value = { layout: { "Umbraco.BlockList": [] }, contentData: [], settingsData: [] };
        this.groups = [];
        this.blockListConfig = null;
        this.config = null;
        this.isMounted = false;

        this.propertyContext = new UmbPropertyContext(this);
        this.propertyContext.setAlias("calendarGroupBlocks");
        this.datasetContext = new MockPropertyDatasetContext(this);
    }

    async connectedCallback() {
        super.connectedCallback();

        const workspace = new MockWorkspaceContext(this);
        const variant = new MockVariantWorkspaceContext(this);
        const clipboardMock = new MockClipboardPropertyContext(this);

        new UmbContextProviderController(this, UMB_WORKSPACE_CONTEXT, workspace);
        new UmbContextProviderController(this, UMB_VARIANT_WORKSPACE_CONTEXT, variant);
        new UmbContextProviderController(this, UMB_CLIPBOARD_PROPERTY_CONTEXT_TOKEN, clipboardMock);
        new UmbContextProviderController(this, UMB_PROPERTY_DATASET_CONTEXT_TOKEN, this.datasetContext);

        const globalTargetHost = document.querySelector("umb-backoffice");
        if (globalTargetHost) {
            new UmbContextProviderController(globalTargetHost, UMB_CLIPBOARD_PROPERTY_CONTEXT_TOKEN, clipboardMock);

            const globalDatasetProxyFallback = {
                api: "default",
                getName: () => "Calendar Group Dataset",
                getUnique: () => "dataset-unique-id",
                getEntityType: () => "genesisCalendar.configuration.groups",
                setPropertyValue: (alias, val) => {
                    this.datasetContext.setPropertyValue(alias, val);
                    this.propertyContext?.setValue(val);
                },
                propertyValueByAlias: (alias) => this.datasetContext.propertyValueByAlias(alias),
                getPropertyValue: (alias) => this.datasetContext.getPropertyValue(alias)
            };
            new UmbContextProviderController(globalTargetHost, "UmbPropertyDatasetContext", globalDatasetProxyFallback);
        }

        await this.loadBlockListConfig();
        await this.loadGroups();

        this.isMounted = true;
    }

    async loadBlockListConfig() {
        this.blockListConfig = [
            {
                alias: "blocks",
                value: [{
                    // ⭐ IMPORTANT: Paste your real Element Type GUID back in here!
                    contentElementTypeKey: "4ffe6bec-43b0-49b9-9373-63c542e95ab3",
                    label: "Group Content Element"
                }]
            }
        ];
        const configCollection = new UmbPropertyEditorConfigCollection(this.blockListConfig);
        this.config = configCollection;
        this.propertyContext?.setConfig(configCollection);
    }

    async loadGroups() {
        try {
            const response = await umbHttpClient.get({
                url: "/umbraco/management/api/v1/genesis/calendar/groups",
                security: [{ scheme: "bearer", type: "http" }]
            });

            this.groups = response.data || [];

            const aggregatedModel = {
                layout: { "Umbraco.BlockList": [] },
                contentData: [],
                settingsData: []
            };

            for (const group of this.groups) {
                if (group.blockListJson) {
                    const block = typeof group.blockListJson === "string" ? JSON.parse(group.blockListJson) : group.blockListJson;
                    if (block.contentKey) {
                        aggregatedModel.layout["Umbraco.BlockList"].push({ contentKey: block.contentKey, settingsKey: block.settingsKey });
                        if (block.content) aggregatedModel.contentData.push(block.content);
                        if (block.settings) aggregatedModel.settingsData.push(block.settings);
                    }
                }
            }

            this.value = aggregatedModel;
            this.datasetContext.setPropertyValue("calendarGroupBlocks", this.value);
            this.requestUpdate();

        } catch (error) {
            console.error("Failed to load groups:", error);
        }
    }

    blockEditorToGroups(model) {
        if (!model) return [];

        let layoutArray = [];
        if (Array.isArray(model.layout)) layoutArray = model.layout;
        else if (model.layout && model.layout["Umbraco.BlockList"]) layoutArray = model.layout["Umbraco.BlockList"];

        if (layoutArray.length === 0) return [];

        return layoutArray.map(layoutItem => {
            const content = model.contentData ? model.contentData.find(x => x.key === layoutItem.contentKey) : null;
            const settings = model.settingsData ? model.settingsData.find(x => x.key === layoutItem.settingsKey) : null;

            let blockName = "Untitled Group";
            if (content && Array.isArray(content.values)) {
                const titleProp = content.values.find(p => p.alias === 'title');
                if (titleProp && titleProp.value) blockName = titleProp.value;
            }

            return {
                key: content?.key || crypto.randomUUID(),
                name: blockName,
                blockListJson: {
                    contentKey: layoutItem.contentKey,
                    settingsKey: layoutItem.settingsKey,
                    content: content,
                    settings: settings
                }
            };
        });
    }

    async onSaveClick() {
        const currentValue = this.datasetContext.getPropertyValue("calendarGroupBlocks");
        if (!currentValue) return;

        const groupsToSave = this.blockEditorToGroups(currentValue);

        try {
            await umbHttpClient.post({
                url: "/umbraco/management/api/v1/genesis/calendar/groups",
                body: groupsToSave,
                security: [{ scheme: "bearer", type: "http" }]
            });
            alert("Blocks saved successfully!");
        } catch (err) {
            console.error("Save failed", err);
        }
    }

    render() {
        const ready = this.isMounted && this.config && this.propertyContext;
        return html`
            <div class="workspace-container" style="display: flex; flex-direction: column; height: 100%; position: relative;">
                
                <div style="padding: 24px; flex: 1; overflow-y: auto; padding-bottom: 80px;">
                    <h2>Genesis Calendar Groups</h2>
                    ${ready
                ? html`
                            <umb-context-provider key="UmbPropertyDatasetContext" .value=${this.datasetContext}>
                                <umb-property
                                    alias="calendarGroupBlocks"
                                    hide-label
                                    no-margin
                                    property-editor-ui-alias="Umb.PropertyEditorUi.BlockList"
                                    .config=${this.config}>
                                </umb-property>
                            </umb-context-provider>
                        `
                : html`<umb-load-indicator></umb-load-indicator>`
            }
                </div>

                <div class="footer" style="position: sticky; bottom: 0; background: var(--uui-color-surface); padding: 16px 24px; border-top: 1px solid var(--uui-color-border); display: flex; justify-content: flex-end; z-index: 100;">
                    <uui-button look="primary" color="positive" label="Save groups" @click=${this.onSaveClick}>
                        Save groups
                    </uui-button>
                </div>
            </div>
        `;
    }
}

customElements.define("genesis-calendar-groups-workspace", GenesisCalendarGroupsWorkspace);
export default GenesisCalendarGroupsWorkspace;

Phase 4: The C# Backend (Memory vs. Database)

To completely isolate frontend Context API bugs from backend SQL schema bugs, this POC uses a static List in C# as an “in-memory” database. This guarantees the backend will successfully catch and return the JSON without failing on Entity Framework/NPoco mapping errors.

Also, ASP.NET Core will throw a 400 Bad Request if you try to catch the complex Block List payload as a generic object. You must define a DTO (CalendarGroupDto) so the JSON deserializer knows how to map it.

The complete GenesisCalendarGroupsController.cs:

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Management.Controllers;
using Asp.Versioning;
using System.Collections.Generic;

namespace MyUmbracoProjecttestSQL11.Controllers
{ 
    public class CalendarGroupDto
    {
        public string Key { get; set; }
        public string Name { get; set; }
        public object BlockListJson { get; set; }
    }
    [ApiVersion("1.0")]
    [ApiExplorerSettings(GroupName = "Genesis Calendar")]
    [Route("umbraco/management/api/v1/genesis/calendar")]
    [MapToApi("management")]
    public class GenesisCalendarGroupsController : ManagementApiControllerBase
    {
        private static List<CalendarGroupDto> _mockDatabase = new List<CalendarGroupDto>();

        [HttpGet("groups")]
        public IActionResult GetGroups()
        {
            return Ok(_mockDatabase);
        }

        // 2. Tell the endpoint to expect our new class
        [HttpPost("groups")]
        public IActionResult SaveGroups([FromBody] List<CalendarGroupDto> payload)
        {
            _mockDatabase = payload;
            return Ok();
        }
    }
}

Final_run_POC

Once you align those Context API discriminators, strip the conflicting HTML wrappers, and map the layout payload accurately, the Block List editor mounts perfectly inside custom routing. Hope this helps you get it across the finish line!

Regards,
Shekhar