Compare commits

...
Author SHA1 Message Date
Mads Rasmussen 66c15e9cce Update package-lock.json 2025-08-11 15:46:37 +02:00
Mads Rasmussen 35caad51f7 Merge branch 'main' into v16/poc/realtime 2025-08-11 15:46:06 +02:00
Mads Rasmussen 377e3db51b do not provide 2025-05-20 13:49:27 +02:00
Mads Rasmussen b7157b1e12 listen to updated events from tree 2025-05-20 13:17:47 +02:00
Mads Rasmussen 3588615bc1 dispatch entity events events 2025-05-20 13:14:55 +02:00
Mads Rasmussen 157155f55f generic entity event 2025-05-20 13:03:13 +02:00
Mads Rasmussen 6b1650c937 fix imports 2025-05-20 13:02:54 +02:00
Mads Rasmussen f292eb88cf align entity events 2025-05-20 12:54:48 +02:00
Mads Rasmussen 39faa8e6fc wip server event context 2025-05-20 12:28:56 +02:00
Mads Rasmussen 173c244a52 add signal r dependency 2025-05-20 12:21:56 +02:00
20 changed files with 270 additions and 14 deletions
+112 -7
View File
@@ -1237,6 +1237,19 @@
"react": ">=16"
}
},
"node_modules/@microsoft/signalr": {
"version": "8.0.17",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.17.tgz",
"integrity": "sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.5.10"
}
},
"node_modules/@mswjs/cookies": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz",
@@ -5448,6 +5461,18 @@
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -8402,6 +8427,15 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -8412,6 +8446,15 @@
"node": ">=0.8.x"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -8602,6 +8645,16 @@
}
}
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -12175,7 +12228,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
@@ -12203,21 +12255,18 @@
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
@@ -14137,6 +14186,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -14152,7 +14213,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -14223,6 +14283,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-lit": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz",
@@ -14588,6 +14654,12 @@
"node": ">=10.13.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -14923,7 +14995,6 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"dev": true,
"license": "MIT"
},
"node_modules/set-function-length": {
@@ -15742,6 +15813,30 @@
"node": ">=0.6"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
@@ -16544,6 +16639,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -17091,7 +17196,6 @@
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.3.0"
@@ -17318,6 +17422,7 @@
"name": "@umbraco-backoffice/core",
"dependencies": {
"@hey-api/client-fetch": "^0.12.0",
"@microsoft/signalr": "^8.0.7",
"@types/diff": "^7.0.2",
"diff": "^7.0.0",
"uuid": "^11.1.0"
@@ -1,11 +1,11 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
import { UmbRequestReloadStructureForEntityEvent } from '../../request-reload-structure-for-entity.event.js';
import type { MetaEntityActionDeleteKind } from './types.js';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
export class UmbDeleteEntityAction<
MetaKind extends MetaEntityActionDeleteKind = MetaEntityActionDeleteKind,
@@ -1,8 +1,8 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
import { UmbRequestReloadStructureForEntityEvent } from '../../request-reload-structure-for-entity.event.js';
import type { UmbDuplicateRepository } from './duplicate-repository.interface.js';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
export class UmbDuplicateEntityAction extends UmbEntityActionBase<any> {
override async execute() {
@@ -0,0 +1,10 @@
import type { UmbEntityActionEventArgs } from './entity-action.event.js';
import { UmbEntityActionEvent } from './entity-action.event.js';
export class UmbEntityCreatedEvent extends UmbEntityActionEvent {
static readonly TYPE = 'entity-created';
constructor(args: UmbEntityActionEventArgs) {
super(UmbEntityCreatedEvent.TYPE, args);
}
}
@@ -0,0 +1,12 @@
import type { UmbEntityActionEventArgs } from './entity-action.event.js';
import { UmbEntityActionEvent } from './entity-action.event.js';
interface UmbGenericEntityActionEventArgs extends UmbEntityActionEventArgs {
type: string;
}
export class UmbEntityEvent extends UmbEntityActionEvent {
constructor(args: UmbGenericEntityActionEventArgs) {
super(args.type, args);
}
}
@@ -0,0 +1,8 @@
export * from './entity-action.event.js';
export * from './entity-created.event.js';
export * from './entity-deleted.event.js';
export * from './entity-updated.event.js';
export * from './entity.event.js';
export * from './request-reload-structure-for-entity.event.js';
export * from './request-reload-children-of-entity.event.js';
@@ -4,13 +4,9 @@ export * from './common/index.js';
export * from './constants.js';
export * from './entity-action-base.js';
export * from './entity-action-list.element.js';
export * from './entity-action.event.js';
export * from './has-children/index.js';
export * from './entity-updated.event.js';
export * from './entity-deleted.event.js';
export * from './event/index.js';
export type * from './types.js';
export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js';
export { UmbRequestReloadChildrenOfEntityEvent } from './request-reload-children-of-entity.event.js';
export { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from './default/default.action.kind.js';
@@ -2,6 +2,7 @@ import { UMB_AUTH_CONTEXT } from './auth/auth.context.token.js';
import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerElement } from './components/index.js';
import { UmbActionEventContext } from './action/action-event.context.js';
import { manifests as coreManifests } from './manifests.js';
import { UmbServerEventManager } from './server/index.js';
import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UmbExtensionsApiInitializer, type UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
@@ -20,6 +21,8 @@ export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
new UmbExtensionsApiInitializer(host, extensionRegistry, 'treeStore', [host]);
new UmbExtensionsApiInitializer(host, extensionRegistry, 'itemStore', [host]);
new UmbServerEventManager(host);
extensionRegistry.registerMany(coreManifests);
const notificationContainerElement = new UmbBackofficeNotificationContainerElement();
@@ -7,6 +7,7 @@
"generate:server-api": "openapi-ts --file openapi-ts.config.js"
},
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"@types/diff": "^7.0.2",
"diff": "^7.0.0",
"uuid": "^11.1.0",
@@ -0,0 +1 @@
export { UMB_SERVER_EVENT_CONTEXT } from './server-event.context.token.js';
@@ -0,0 +1 @@
export * from './server-event.manager.js';
@@ -0,0 +1,105 @@
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
// eslint-disable-next-line local-rules/enforce-umbraco-external-imports
import type { HubConnection } from '@microsoft/signalr';
// eslint-disable-next-line local-rules/enforce-umbraco-external-imports
import { HubConnectionBuilder } from '@microsoft/signalr';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbEntityEvent } from '@umbraco-cms/backoffice/entity-action';
interface UmbServerEventModel {
eventSource: string;
eventType: string;
key: string;
}
export class UmbServerEventManager extends UmbControllerBase {
#connection?: HubConnection;
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host);
this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
this.#authContext = context;
this.#observeIsAuthorized();
});
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
this.#actionEventContext = context;
});
}
#observeIsAuthorized() {
this.observe(this.#authContext?.isAuthorized, async (isAuthorized) => {
if (isAuthorized) {
const token = await this.#authContext?.getLatestToken();
if (token) {
this.#initHubConnection(token);
} else {
throw new Error('No auth token found');
}
} else {
this.#connection?.stop();
this.#connection = undefined;
}
});
}
#initHubConnection(token: string) {
this.#connection = new HubConnectionBuilder()
.withUrl('https://localhost:44339/umbraco/serverEventHub', {
accessTokenFactory: () => token,
})
.build();
this.#connection.on('notify', (payload: UmbServerEventModel) => {
console.log('payloadReceived', payload);
// TODO: Only for POC purposes, replace with a proper mapping
const eventSourceToEntity: Record<any, any> = {
'Umbraco:CMS:Document': 'document',
'Umbraco:CMS:DocumentType': 'document-type',
'Umbraco:CMS:Media': 'media',
'Umbraco:CMS:MediaType': 'media-type',
'Umbraco:CMS:Member': 'member',
'Umbraco:CMS:MemberType': 'member-type',
'Umbraco:CMS:MemberGroup': 'member-group',
'Umbraco:CMS:DataType': 'data-type',
};
// TODO: Only for POC purposes, replace with a proper mapping
const eventTypeToAction: Record<any, any> = {
Created: 'entity-created',
Updated: 'entity-updated',
Deleted: 'entity-deleted',
Trashed: 'entity-trashed',
};
const event = new UmbEntityEvent({
type: eventTypeToAction[payload.eventType],
entityType: eventSourceToEntity[payload.eventSource],
unique: payload.key,
});
this.#actionEventContext?.dispatchEvent(event);
});
this.#connection
.start()
.then(function () {
console.log('Connected!');
})
.catch(function (err) {
console.log(err);
});
this.#connection.onclose((err?: Error) => {
if (err) {
console.error('Connection closed with error: ', err);
}
});
}
}
@@ -0,0 +1 @@
export type * from './server-event.manager.js';
@@ -1,3 +1,4 @@
export * from './event/index.js';
export * from './server-connection.js';
export * from './server.context-token.js';
export * from './server.context.js';
@@ -12,6 +12,8 @@ import { UMB_SECTION_CONTEXT, UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/b
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
UmbEntityCreatedEvent,
UmbEntityUpdatedEvent,
UmbHasChildrenEntityContext,
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
@@ -323,6 +325,11 @@ export abstract class UmbTreeItemContextBase<
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
this.#actionEventContext?.addEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
});
}
@@ -480,6 +487,11 @@ export abstract class UmbTreeItemContextBase<
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
};
override destroy(): void {