diff --git a/src/Umbraco.Core/Services/DictionaryItemService.cs b/src/Umbraco.Core/Services/DictionaryItemService.cs
index c8cb2605b5..108ab28c25 100644
--- a/src/Umbraco.Core/Services/DictionaryItemService.cs
+++ b/src/Umbraco.Core/Services/DictionaryItemService.cs
@@ -159,7 +159,25 @@ internal sealed class DictionaryItemService : RepositoryService, IDictionaryItem
///
public async Task> UpdateAsync(
IDictionaryItem dictionaryItem, Guid userKey)
- => await SaveAsync(
+ {
+ // Create and update dates aren't tracked for dictionary items. They exist on IDictionaryItem due to the
+ // inheritance from IEntity, but we don't store them.
+ // However we have logic in ServerEventSender that will provide SignalR events for created and update operations,
+ // where these dates are used to distinguish between the two (whether or not the entity has an identity cannot
+ // be used here, as these events fire after persistence when the identity is known for both creates and updates).
+ // So ensure we set something that can be distinguished here.
+ if (dictionaryItem.CreateDate == default)
+ {
+ dictionaryItem.CreateDate = DateTime.MinValue;
+ }
+
+ if (dictionaryItem.UpdateDate == default)
+ {
+ // TODO (V17): To align with updates of system dates, this needs to change to DateTime.UtcNow.
+ dictionaryItem.UpdateDate = DateTime.Now;
+ }
+
+ return await SaveAsync(
dictionaryItem,
() =>
{
@@ -174,6 +192,7 @@ internal sealed class DictionaryItemService : RepositoryService, IDictionaryItem
AuditType.Save,
"Update DictionaryItem",
userKey);
+ }
///
public async Task> DeleteAsync(Guid id, Guid userKey)
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index 3915d0cc54..2b5832dd62 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -1257,6 +1257,19 @@
"react": ">=16"
}
},
+ "node_modules/@microsoft/signalr": {
+ "version": "9.0.6",
+ "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz",
+ "integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==",
+ "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",
@@ -3552,6 +3565,10 @@
"resolved": "src/packages/log-viewer",
"link": true
},
+ "node_modules/@umbraco-backoffice/management-api": {
+ "resolved": "src/packages/management-api",
+ "link": true
+ },
"node_modules/@umbraco-backoffice/markdown": {
"resolved": "src/packages/markdown-editor",
"link": true
@@ -3624,6 +3641,10 @@
"resolved": "src/packages/settings",
"link": true
},
+ "node_modules/@umbraco-backoffice/signalr": {
+ "resolved": "src/external/signalr",
+ "link": true
+ },
"node_modules/@umbraco-backoffice/static-file": {
"resolved": "src/packages/static-file",
"link": true
@@ -5030,6 +5051,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",
@@ -8011,6 +8044,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",
@@ -8021,6 +8063,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",
@@ -8192,6 +8243,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",
@@ -11777,7 +11838,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"
@@ -11805,21 +11865,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",
@@ -13670,6 +13727,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.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -13685,7 +13754,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"
@@ -13756,6 +13824,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",
@@ -14137,6 +14211,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.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -14504,7 +14584,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": {
@@ -15409,6 +15488,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",
@@ -16180,6 +16283,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",
@@ -16729,7 +16842,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"
@@ -16910,6 +17022,12 @@
"rxjs": "^7.8.2"
}
},
+ "src/external/signalr": {
+ "name": "@umbraco-backoffice/signalr",
+ "dependencies": {
+ "@microsoft/signalr": "9.0.6"
+ }
+ },
"src/external/tiptap": {
"name": "@umbraco-backoffice/external-tiptap",
"dependencies": {
@@ -17007,6 +17125,9 @@
"src/packages/log-viewer": {
"name": "@umbraco-backoffice/log-viewer"
},
+ "src/packages/management-api": {
+ "name": "@umbraco-backoffice/management-api"
+ },
"src/packages/markdown-editor": {
"name": "@umbraco-backoffice/markdown"
},
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index 3b931334af..6ec54b731f 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -60,6 +60,7 @@
"./lit-element": "./dist-cms/packages/core/lit-element/index.js",
"./localization": "./dist-cms/packages/core/localization/index.js",
"./log-viewer": "./dist-cms/packages/log-viewer/index.js",
+ "./management-api": "./dist-cms/packages/management-api/index.js",
"./markdown-editor": "./dist-cms/packages/markdown-editor/index.js",
"./media-type": "./dist-cms/packages/media/media-types/index.js",
"./media": "./dist-cms/packages/media/media/index.js",
@@ -126,6 +127,7 @@
"./external/monaco-editor": "./dist-cms/external/monaco-editor/index.js",
"./external/openid": "./dist-cms/external/openid/index.js",
"./external/rxjs": "./dist-cms/external/rxjs/index.js",
+ "./external/signalr": "./dist-cms/external/signalr/index.js",
"./external/tiptap": "./dist-cms/external/tiptap/index.js",
"./external/uui": "./dist-cms/external/uui/index.js"
},
diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts
index 2c0b5fd876..b284635d58 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts
@@ -15,6 +15,7 @@ const CORE_PACKAGES = [
import('../../packages/block/umbraco-package.js'),
import('../../packages/clipboard/umbraco-package.js'),
import('../../packages/code-editor/umbraco-package.js'),
+ import('../../packages/content/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),
@@ -24,6 +25,7 @@ const CORE_PACKAGES = [
import('../../packages/help/umbraco-package.js'),
import('../../packages/language/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
+ import('../../packages/management-api/umbraco-package.js'),
import('../../packages/markdown-editor/umbraco-package.js'),
import('../../packages/media/umbraco-package.js'),
import('../../packages/members/umbraco-package.js'),
@@ -48,7 +50,6 @@ const CORE_PACKAGES = [
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/user/umbraco-package.js'),
import('../../packages/webhook/umbraco-package.js'),
- import('../../packages/content/umbraco-package.js'),
];
@customElement('umb-backoffice')
diff --git a/src/Umbraco.Web.UI.Client/src/external/signalr/index.ts b/src/Umbraco.Web.UI.Client/src/external/signalr/index.ts
new file mode 100644
index 0000000000..324f4d0a56
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/external/signalr/index.ts
@@ -0,0 +1 @@
+export * from '@microsoft/signalr';
diff --git a/src/Umbraco.Web.UI.Client/src/external/signalr/package.json b/src/Umbraco.Web.UI.Client/src/external/signalr/package.json
new file mode 100644
index 0000000000..08e7abbc27
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/external/signalr/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@umbraco-backoffice/signalr",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@microsoft/signalr": "9.0.6"
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/external/signalr/vite.config.ts b/src/Umbraco.Web.UI.Client/src/external/signalr/vite.config.ts
new file mode 100644
index 0000000000..6729a413c5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/external/signalr/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite';
+import { rmSync } from 'fs';
+import { getDefaultConfig } from '../../vite-config-base';
+
+const dist = '../../../dist-cms/external/signalr';
+
+// delete the unbundled dist folder
+rmSync(dist, { recursive: true, force: true });
+
+export default defineConfig({
+ ...getDefaultConfig({
+ dist,
+ base: '/umbraco/backoffice/external/signalr',
+ entry: {
+ index: './index.ts',
+ },
+ }),
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts
index dbf3edb45f..5c401f6c37 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts
@@ -51,7 +51,6 @@ export class UmbBlockElementManager();
readonly structure = new UmbContentTypeStructureManager(
this,
@@ -91,18 +90,6 @@ export class UmbBlockElementManager {
- // Make a map of the data type unique and editorAlias:
- this.#dataTypeSchemaAliasMap = new Map(
- dataTypes.map((dataType) => {
- return [dataType.unique, dataType.propertyEditorSchemaAlias];
- }),
- );
- },
- null,
- );
}
public isLoaded() {
@@ -217,8 +204,9 @@ export class UmbBlockElementManager();
+
+export { dataTypeDetailCache };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/data-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts
similarity index 67%
rename from src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/data-type-detail.server.data-source.ts
rename to src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts
index 9963b80846..a79c9437c5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/data-type-detail.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts
@@ -1,31 +1,25 @@
-import type { UmbDataTypeDetailModel, UmbDataTypePropertyValueModel } from '../../types.js';
-import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js';
+import type { UmbDataTypeDetailModel, UmbDataTypePropertyValueModel } from '../../../types.js';
+import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../../entity.js';
+import { UmbManagementApiDataTypeDetailDataRequestManager } from './data-type-detail.server.request-manager.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
import type {
CreateDataTypeRequestModel,
+ DataTypeResponseModel,
UpdateDataTypeRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
-import { DataTypeService } from '@umbraco-cms/backoffice/external/backend-api';
-import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
-import { tryExecute } from '@umbraco-cms/backoffice/resources';
+import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
/**
* A data source for the Data Type that fetches data from the server
* @class UmbDataTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
-export class UmbDataTypeServerDataSource implements UmbDetailDataSource {
- #host: UmbControllerHost;
-
- /**
- * Creates an instance of UmbDataTypeServerDataSource.
- * @param {UmbControllerHost} host - The controller host for this controller to be appended to
- * @memberof UmbDataTypeServerDataSource
- */
- constructor(host: UmbControllerHost) {
- this.#host = host;
- }
+export class UmbDataTypeServerDataSource
+ extends UmbControllerBase
+ implements UmbDetailDataSource
+{
+ #detailRequestManager = new UmbManagementApiDataTypeDetailDataRequestManager(this);
/**
* Creates a new Data Type scaffold
@@ -57,23 +51,9 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource,
- };
-
- return { data: dataType };
+ return { data: data ? this.#mapServerResponseModelToEntityDetailModel(data) : undefined, error };
}
/**
@@ -99,18 +79,9 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource,
+ };
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts
new file mode 100644
index 0000000000..46c49519dd
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts
@@ -0,0 +1,27 @@
+/* eslint-disable local-rules/no-direct-api-import */
+import { dataTypeDetailCache } from './data-type-detail.server.cache.js';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import {
+ DataTypeService,
+ type CreateDataTypeRequestModel,
+ type DataTypeResponseModel,
+ type UpdateDataTypeRequestModel,
+} from '@umbraco-cms/backoffice/external/backend-api';
+import { UmbManagementApiDetailDataRequestManager } from '@umbraco-cms/backoffice/management-api';
+
+export class UmbManagementApiDataTypeDetailDataRequestManager extends UmbManagementApiDetailDataRequestManager<
+ DataTypeResponseModel,
+ UpdateDataTypeRequestModel,
+ CreateDataTypeRequestModel
+> {
+ constructor(host: UmbControllerHost) {
+ super(host, {
+ create: (body: CreateDataTypeRequestModel) => DataTypeService.postDataType({ body }),
+ read: (id: string) => DataTypeService.getDataTypeById({ path: { id } }),
+ update: (id: string, body: UpdateDataTypeRequestModel) => DataTypeService.putDataTypeById({ path: { id }, body }),
+ delete: (id: string) => DataTypeService.deleteDataTypeById({ path: { id } }),
+ dataCache: dataTypeDetailCache,
+ serverEventSource: 'Umbraco:CMS:DataType',
+ });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.repository.ts
index faf5883411..f79ee9a5b2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.repository.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.repository.ts
@@ -1,6 +1,6 @@
import type { UmbDocumentTypeDetailModel } from '../../types.js';
-import { UmbDocumentTypeDetailServerDataSource } from './document-type-detail.server.data-source.js';
import { UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT } from './document-type-detail.store.context-token.js';
+import { UmbDocumentTypeDetailServerDataSource } from './server-data-source/document-type-detail.server.data-source.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbDocumentTypeDetailRepository extends UmbDetailRepositoryBase {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/index.ts
index 68d24e6f3e..356cd69c01 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/index.ts
@@ -1,2 +1,2 @@
export { UmbDocumentTypeDetailRepository } from './document-type-detail.repository.js';
-export * from './document-type-detail.server.data-source.js';
+export * from './server-data-source/document-type-detail.server.data-source.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache.ts
new file mode 100644
index 0000000000..208efdacbd
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache.ts
@@ -0,0 +1,6 @@
+import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
+import { UmbManagementApiDetailDataCache } from '@umbraco-cms/backoffice/management-api';
+
+const documentTypeDetailCache = new UmbManagementApiDetailDataCache();
+
+export { documentTypeDetailCache };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.data-source.ts
similarity index 82%
rename from src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts
rename to src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.data-source.ts
index b937292ce5..9ac3abe0a1 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.data-source.ts
@@ -1,32 +1,26 @@
-import type { UmbDocumentTypeDetailModel } from '../../types.js';
-import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../../entity.js';
+import type { UmbDocumentTypeDetailModel } from '../../../types.js';
+import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../../../entity.js';
+import { UmbManagementApiDocumentTypeDetailDataRequestManager } from './document-type-detail.server.request-manager.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
import type {
CreateDocumentTypeRequestModel,
+ DocumentTypeResponseModel,
UpdateDocumentTypeRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
-import { DocumentTypeService } from '@umbraco-cms/backoffice/external/backend-api';
-import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
-import { tryExecute } from '@umbraco-cms/backoffice/resources';
import type { UmbPropertyContainerTypes, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
+import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
/**
* A data source for the Document Type that fetches data from the server
* @class UmbDocumentTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
-export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSource {
- #host: UmbControllerHost;
-
- /**
- * Creates an instance of UmbDocumentTypeServerDataSource.
- * @param {UmbControllerHost} host - The controller host for this controller to be appended to
- * @memberof UmbDocumentTypeServerDataSource
- */
- constructor(host: UmbControllerHost) {
- this.#host = host;
- }
+export class UmbDocumentTypeDetailServerDataSource
+ extends UmbControllerBase
+ implements UmbDetailDataSource
+{
+ #detailRequestManager = new UmbManagementApiDocumentTypeDetailDataRequestManager(this);
/**
* Creates a new Document Type scaffold
@@ -74,63 +68,9 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc
async read(unique: string) {
if (!unique) throw new Error('Unique is missing');
- const { data, error } = await tryExecute(
- this.#host,
- DocumentTypeService.getDocumentTypeById({ path: { id: unique } }),
- );
+ const { data, error } = await this.#detailRequestManager.read(unique);
- if (error || !data) {
- return { error };
- }
-
- // TODO: make data mapper to prevent errors
- const DocumentType: UmbDocumentTypeDetailModel = {
- entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE,
- unique: data.id,
- name: data.name,
- alias: data.alias,
- description: data.description ?? '',
- icon: data.icon,
- allowedAtRoot: data.allowedAsRoot,
- variesByCulture: data.variesByCulture,
- variesBySegment: data.variesBySegment,
- isElement: data.isElement,
- properties: data.properties.map((property) => {
- return {
- id: property.id,
- unique: property.id,
- container: property.container,
- sortOrder: property.sortOrder,
- alias: property.alias,
- name: property.name,
- description: property.description,
- dataType: { unique: property.dataType.id },
- variesByCulture: property.variesByCulture,
- variesBySegment: property.variesBySegment,
- validation: property.validation,
- appearance: property.appearance,
- };
- }),
- containers: data.containers as UmbPropertyTypeContainerModel[],
- allowedContentTypes: data.allowedDocumentTypes.map((allowedDocumentType) => {
- return {
- contentType: { unique: allowedDocumentType.documentType.id },
- sortOrder: allowedDocumentType.sortOrder,
- };
- }),
- compositions: data.compositions.map((composition) => {
- return {
- contentType: { unique: composition.documentType.id },
- compositionType: composition.compositionType,
- };
- }),
- allowedTemplates: data.allowedTemplates,
- defaultTemplate: data.defaultTemplate ? { id: data.defaultTemplate.id } : null,
- cleanup: data.cleanup,
- collection: data.collection ? { unique: data.collection?.id } : null,
- };
-
- return { data: DocumentType };
+ return { data: data ? this.#mapServerResponseModelToEntityDetailModel(data) : undefined, error };
}
/**
@@ -190,18 +130,9 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc
collection: model.collection?.unique ? { id: model.collection?.unique } : null,
};
- const { data, error } = await tryExecute(
- this.#host,
- DocumentTypeService.postDocumentType({
- body: body,
- }),
- );
+ const { data, error } = await this.#detailRequestManager.create(body);
- if (data) {
- return this.read(data as any);
- }
-
- return { error };
+ return { data: data ? this.#mapServerResponseModelToEntityDetailModel(data) : undefined, error };
}
/**
@@ -266,19 +197,9 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc
collection: model.collection?.unique ? { id: model.collection?.unique } : null,
};
- const { error } = await tryExecute(
- this.#host,
- DocumentTypeService.putDocumentTypeById({
- path: { id: model.unique },
- body: body,
- }),
- );
+ const { data, error } = await this.#detailRequestManager.update(model.unique, body);
- if (!error) {
- return this.read(model.unique);
- }
-
- return { error };
+ return { data: data ? this.#mapServerResponseModelToEntityDetailModel(data) : undefined, error };
}
/**
@@ -289,12 +210,55 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc
*/
async delete(unique: string) {
if (!unique) throw new Error('Unique is missing');
+ return this.#detailRequestManager.delete(unique);
+ }
- return tryExecute(
- this.#host,
- DocumentTypeService.deleteDocumentTypeById({
- path: { id: unique },
+ // TODO: change this to a mapper extension when the endpoints returns a $type for DocumentTypeResponseModel
+ #mapServerResponseModelToEntityDetailModel(data: DocumentTypeResponseModel): UmbDocumentTypeDetailModel {
+ return {
+ entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE,
+ unique: data.id,
+ name: data.name,
+ alias: data.alias,
+ description: data.description ?? '',
+ icon: data.icon,
+ allowedAtRoot: data.allowedAsRoot,
+ variesByCulture: data.variesByCulture,
+ variesBySegment: data.variesBySegment,
+ isElement: data.isElement,
+ properties: data.properties.map((property) => {
+ return {
+ id: property.id,
+ unique: property.id,
+ container: property.container,
+ sortOrder: property.sortOrder,
+ alias: property.alias,
+ name: property.name,
+ description: property.description,
+ dataType: { unique: property.dataType.id },
+ variesByCulture: property.variesByCulture,
+ variesBySegment: property.variesBySegment,
+ validation: property.validation,
+ appearance: property.appearance,
+ };
}),
- );
+ containers: data.containers as UmbPropertyTypeContainerModel[],
+ allowedContentTypes: data.allowedDocumentTypes.map((allowedDocumentType) => {
+ return {
+ contentType: { unique: allowedDocumentType.documentType.id },
+ sortOrder: allowedDocumentType.sortOrder,
+ };
+ }),
+ compositions: data.compositions.map((composition) => {
+ return {
+ contentType: { unique: composition.documentType.id },
+ compositionType: composition.compositionType,
+ };
+ }),
+ allowedTemplates: data.allowedTemplates,
+ defaultTemplate: data.defaultTemplate ? { id: data.defaultTemplate.id } : null,
+ cleanup: data.cleanup,
+ collection: data.collection ? { unique: data.collection?.id } : null,
+ };
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts
new file mode 100644
index 0000000000..84e76a3b81
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts
@@ -0,0 +1,28 @@
+/* eslint-disable local-rules/no-direct-api-import */
+import { documentTypeDetailCache } from './document-type-detail.server.cache.js';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import {
+ DocumentTypeService,
+ type CreateDocumentTypeRequestModel,
+ type DocumentTypeResponseModel,
+ type UpdateDocumentTypeRequestModel,
+} from '@umbraco-cms/backoffice/external/backend-api';
+import { UmbManagementApiDetailDataRequestManager } from '@umbraco-cms/backoffice/management-api';
+
+export class UmbManagementApiDocumentTypeDetailDataRequestManager extends UmbManagementApiDetailDataRequestManager<
+ DocumentTypeResponseModel,
+ UpdateDocumentTypeRequestModel,
+ CreateDocumentTypeRequestModel
+> {
+ constructor(host: UmbControllerHost) {
+ super(host, {
+ create: (body: CreateDocumentTypeRequestModel) => DocumentTypeService.postDocumentType({ body }),
+ read: (id: string) => DocumentTypeService.getDocumentTypeById({ path: { id } }),
+ update: (id: string, body: UpdateDocumentTypeRequestModel) =>
+ DocumentTypeService.putDocumentTypeById({ path: { id }, body }),
+ delete: (id: string) => DocumentTypeService.deleteDocumentTypeById({ path: { id } }),
+ dataCache: documentTypeDetailCache,
+ serverEventSource: 'Umbraco:CMS:DocumentType',
+ });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
index 9adeb01d11..b051f6ed3f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
@@ -38,6 +38,7 @@ export class UmbDocumentServerDataSource
throw new Error('Document type unique is missing');
}
+ // TODO: investigate if we can use the repository here instead
const { data } = await new UmbDocumentTypeDetailServerDataSource(this).read(documentTypeUnique);
documentTypeIcon = data?.icon ?? null;
documentTypeCollection = data?.collection ?? null;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts
new file mode 100644
index 0000000000..5e5a0577a9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts
@@ -0,0 +1,89 @@
+import { UmbManagementApiDetailDataCache } from './cache.js';
+import { expect } from '@open-wc/testing';
+
+describe('UmbManagementApiDetailDataCache', () => {
+ let cache: UmbManagementApiDetailDataCache<{ id: string }>;
+
+ beforeEach(() => {
+ cache = new UmbManagementApiDetailDataCache();
+ });
+
+ describe('Public API', () => {
+ describe('properties', () => {});
+
+ describe('methods', () => {
+ it('has a has method', () => {
+ expect(cache).to.have.property('has').that.is.a('function');
+ });
+
+ it('has a set method', () => {
+ expect(cache).to.have.property('set').that.is.a('function');
+ });
+
+ it('has a get method', () => {
+ expect(cache).to.have.property('get').that.is.a('function');
+ });
+
+ it('has a delete method', () => {
+ expect(cache).to.have.property('delete').that.is.a('function');
+ });
+
+ it('has a clear method', () => {
+ expect(cache).to.have.property('clear').that.is.a('function');
+ });
+ });
+ });
+
+ describe('Has', () => {
+ it('returns true if the item exists in the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ expect(cache.has('item1')).to.be.true;
+ });
+
+ it('returns false if the item does not exist in the cache', () => {
+ expect(cache.has('item2')).to.be.false;
+ });
+ });
+
+ describe('Set', () => {
+ it('adds an item to the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ expect(cache.has('item1')).to.be.true;
+ });
+
+ it('updates an existing item in the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ cache.set('item1', { id: 'item1-updated' });
+ expect(cache.get('item1')).to.deep.equal({ id: 'item1-updated' });
+ });
+ });
+
+ describe('Get', () => {
+ it('returns an item from the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ expect(cache.get('item1')).to.deep.equal({ id: 'item1' });
+ });
+
+ it('returns undefined if the item does not exist in the cache', () => {
+ expect(cache.get('item2')).to.be.undefined;
+ });
+ });
+
+ describe('Delete', () => {
+ it('removes an item from the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ cache.delete('item1');
+ expect(cache.has('item1')).to.be.false;
+ });
+ });
+
+ describe('Clear', () => {
+ it('removes all items from the cache', () => {
+ cache.set('item1', { id: 'item1' });
+ cache.set('item2', { id: 'item2' });
+ cache.clear();
+ expect(cache.has('item1')).to.be.false;
+ expect(cache.has('item2')).to.be.false;
+ });
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts
new file mode 100644
index 0000000000..a8b51f3c9e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts
@@ -0,0 +1,67 @@
+// Keep internal
+interface UmbCacheEntryModel {
+ id: string;
+ data: DataModelType;
+}
+
+/**
+ * A runtime cache for storing entity detail data from the Management Api
+ * @class UmbManagementApiDetailDataCache
+ * @template DataModelType
+ */
+export class UmbManagementApiDetailDataCache {
+ #entries: Map> = new Map();
+
+ /**
+ * Checks if an entry exists in the cache
+ * @param {string} id - The ID of the entry to check
+ * @returns {boolean} - True if the entry exists, false otherwise
+ * @memberof UmbManagementApiDetailDataCache
+ */
+ has(id: string): boolean {
+ return this.#entries.has(id);
+ }
+
+ /**
+ * Adds an entry to the cache
+ * @param {string} id - The ID of the entry to add
+ * @param {DataModelType} data - The data to cache
+ * @memberof UmbManagementApiDetailDataCache
+ */
+ set(id: string, data: DataModelType): void {
+ const cacheEntry: UmbCacheEntryModel = {
+ id: id,
+ data,
+ };
+
+ this.#entries.set(id, cacheEntry);
+ }
+
+ /**
+ * Retrieves an entry from the cache
+ * @param {string} id - The ID of the entry to retrieve
+ * @returns {DataModelType | undefined} - The cached entry or undefined if not found
+ * @memberof UmbManagementApiDetailDataCache
+ */
+ get(id: string): DataModelType | undefined {
+ const entry = this.#entries.get(id);
+ return entry ? entry.data : undefined;
+ }
+
+ /**
+ * Deletes an entry from the cache
+ * @param {string} id - The ID of the entry to delete
+ * @memberof UmbManagementApiDetailDataCache
+ */
+ delete(id: string): void {
+ this.#entries.delete(id);
+ }
+
+ /**
+ * Clears all entries from the cache
+ * @memberof UmbManagementApiDetailDataCache
+ */
+ clear(): void {
+ this.#entries.clear();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts
new file mode 100644
index 0000000000..db2228dd86
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts
@@ -0,0 +1,144 @@
+import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js';
+import type { UmbManagementApiDetailDataCache } from './cache.js';
+import {
+ tryExecute,
+ type UmbApiError,
+ type UmbCancelError,
+ type UmbApiResponse,
+ type UmbApiWithErrorResponse,
+} from '@umbraco-cms/backoffice/resources';
+import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+
+export interface UmbManagementApiDetailDataRequestManagerArgs<
+ DetailResponseModelType,
+ CreateRequestModelType,
+ UpdateRequestModelType,
+> {
+ create: (data: CreateRequestModelType) => Promise>;
+ read: (id: string) => Promise>;
+ update: (id: string, data: UpdateRequestModelType) => Promise>;
+ delete: (id: string) => Promise>;
+ dataCache: UmbManagementApiDetailDataCache;
+ serverEventSource: string;
+}
+
+export class UmbManagementApiDetailDataRequestManager<
+ DetailResponseModelType,
+ CreateRequestModelType,
+ UpdateRequestModelType,
+> extends UmbControllerBase {
+ #dataCache: UmbManagementApiDetailDataCache;
+ #serverEventSource: string;
+ #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE;
+
+ #create;
+ #read;
+ #update;
+ #delete;
+ #isConnectedToServerEvents = false;
+
+ constructor(
+ host: UmbControllerHost,
+ args: UmbManagementApiDetailDataRequestManagerArgs<
+ DetailResponseModelType,
+ CreateRequestModelType,
+ UpdateRequestModelType
+ >,
+ ) {
+ super(host);
+
+ this.#create = args.create;
+ this.#read = args.read;
+ this.#update = args.update;
+ this.#delete = args.delete;
+
+ this.#dataCache = args.dataCache;
+ this.#serverEventSource = args.serverEventSource;
+
+ this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => {
+ this.#serverEventContext = context;
+ this.#observeServerEvents();
+ });
+ }
+
+ async create(data: CreateRequestModelType): Promise> {
+ const { data: createdId, error } = await tryExecute(this, this.#create(data));
+
+ if (!error) {
+ return this.read(createdId as string);
+ }
+
+ return { error };
+ }
+
+ async read(id: string): Promise> {
+ let data: DetailResponseModelType | undefined;
+ let error: UmbApiError | UmbCancelError | undefined;
+
+ // Only read from the cache when we are connected to the server events
+ if (this.#isConnectedToServerEvents && this.#dataCache.has(id)) {
+ data = this.#dataCache.get(id);
+ } else {
+ const { data: serverData, error: serverError } = await tryExecute(this, this.#read(id));
+
+ if (this.#isConnectedToServerEvents && serverData) {
+ this.#dataCache.set(id, serverData);
+ }
+
+ data = serverData;
+ error = serverError;
+ }
+
+ return { data, error };
+ }
+
+ async update(id: string, data: UpdateRequestModelType): Promise> {
+ const { error } = await tryExecute(this, this.#update(id, data));
+
+ if (!error) {
+ return this.read(id);
+ }
+
+ return { error };
+ }
+
+ async delete(id: string): Promise {
+ const { error } = await this.#delete(id);
+
+ // Only update the cache when we are connected to the server events
+ if (this.#isConnectedToServerEvents && !error) {
+ this.#dataCache.delete(id);
+ }
+
+ return { error };
+ }
+
+ #observeServerEvents() {
+ this.observe(
+ this.#serverEventContext?.isConnected,
+ (isConnected) => {
+ /* We purposefully ignore the initial value of isConnected.
+ We only care about whether the connection is established or not (true/false) */
+ if (isConnected === undefined) return;
+ this.#isConnectedToServerEvents = isConnected;
+
+ // Clear the cache if we lose connection to the server events
+ if (this.#isConnectedToServerEvents === false) {
+ this.#dataCache.clear();
+ }
+ },
+ 'umbObserveServerEventsConnection',
+ );
+
+ // Invalidate cache entries when entities are updated or deleted
+ this.observe(
+ this.#serverEventContext?.byEventSourceAndTypes(this.#serverEventSource, ['Updated', 'Deleted']),
+ (event) => {
+ if (!event) return;
+ this.#dataCache.delete(event.key);
+ },
+ 'umbObserveServerEvents',
+ );
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts
new file mode 100644
index 0000000000..890386ff47
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts
@@ -0,0 +1,2 @@
+export * from './detail-data.request-manager.js';
+export * from './cache.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts
new file mode 100644
index 0000000000..5b2e199c84
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts
@@ -0,0 +1,2 @@
+export * from './detail/index.js';
+export * from './server-event/constants.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/manifests.ts
new file mode 100644
index 0000000000..26c13f92b9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/manifests.ts
@@ -0,0 +1,3 @@
+import { manifests as serverEventManifests } from './server-event/manifests.js';
+
+export const manifests: Array = [...serverEventManifests];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/package.json b/src/Umbraco.Web.UI.Client/src/packages/management-api/package.json
new file mode 100644
index 0000000000..d8653e9835
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@umbraco-backoffice/management-api",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build"
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/constants.ts
new file mode 100644
index 0000000000..b69915b2cf
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/constants.ts
@@ -0,0 +1 @@
+export * from './global-context/constants.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/constants.ts
new file mode 100644
index 0000000000..5822d52362
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/constants.ts
@@ -0,0 +1 @@
+export * from './server-event.context-token.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/manifests.ts
new file mode 100644
index 0000000000..6aaa311e9b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/manifests.ts
@@ -0,0 +1,8 @@
+export const manifests: Array = [
+ {
+ type: 'globalContext',
+ alias: 'Umb.GlobalContext.ManagementApi.ServerEvent',
+ name: 'Management Api Server Event Global Context',
+ api: () => import('./server-event.context.js'),
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context-token.ts
new file mode 100644
index 0000000000..223eb624bb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context-token.ts
@@ -0,0 +1,6 @@
+import type { UmbManagementApiServerEventContext } from './server-event.context.js';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+
+export const UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT = new UmbContextToken(
+ 'UmbManagementApiServerEventContext',
+);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts
new file mode 100644
index 0000000000..34f025ebff
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts
@@ -0,0 +1,107 @@
+import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from './server-event.context-token.js';
+import type { UmbManagementApiServerEventModel } from './types.js';
+import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
+import { HubConnectionBuilder, type HubConnection } from '@umbraco-cms/backoffice/external/signalr';
+import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
+import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
+import { filter, Subject } from '@umbraco-cms/backoffice/external/rxjs';
+import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
+
+export class UmbManagementApiServerEventContext extends UmbContextBase {
+ #connection?: HubConnection;
+ #authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
+ #serverContext?: typeof UMB_SERVER_CONTEXT.TYPE;
+
+ #events = new Subject();
+ public readonly events = this.#events.asObservable();
+
+ #isConnected = new UmbBooleanState(undefined);
+ public readonly isConnected = this.#isConnected.asObservable();
+
+ /**
+ * Filters events by the given event source
+ * @param {string} eventSource
+ * @returns {Observable} - The filtered events
+ * @memberof UmbManagementApiServerEventContext
+ */
+ byEventSource(eventSource: string): Observable {
+ return this.#events.asObservable().pipe(filter((event) => event.eventSource === eventSource));
+ }
+
+ /**
+ * Filters events by the given event source and event types
+ * @param {string} eventSource
+ * @param {Array} eventTypes
+ * @returns {Observable} - The filtered events
+ * @memberof UmbManagementApiServerEventContext
+ */
+ byEventSourceAndTypes(eventSource: string, eventTypes: Array): Observable {
+ return this.#events
+ .asObservable()
+ .pipe(filter((event) => event.eventSource === eventSource && eventTypes.includes(event.eventType)));
+ }
+
+ constructor(host: UmbControllerHost) {
+ super(host, UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT);
+
+ this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
+ this.#authContext = context;
+ this.#observeIsAuthorized();
+ });
+
+ this.consumeContext(UMB_SERVER_CONTEXT, (context) => {
+ this.#serverContext = context;
+ });
+ }
+
+ #observeIsAuthorized() {
+ this.observe(this.#authContext?.isAuthorized, async (isAuthorized) => {
+ if (isAuthorized === undefined) return;
+
+ if (isAuthorized) {
+ const token = await this.#authContext?.getLatestToken();
+ if (token) {
+ this.#initHubConnection(token);
+ } else {
+ throw new Error('No auth token found');
+ }
+ } else {
+ this.#isConnected.setValue(false);
+ this.#connection?.stop();
+ this.#connection = undefined;
+ }
+ });
+ }
+
+ #initHubConnection(token: string) {
+ const serverURL = this.#serverContext?.getServerUrl();
+
+ if (!serverURL) {
+ throw new Error('Server URL is not defined in the server context');
+ }
+
+ // TODO: get the url from a server config?
+ const serverEventHubUrl = `${serverURL}/umbraco/serverEventHub`;
+
+ this.#connection = new HubConnectionBuilder()
+ .withUrl(serverEventHubUrl, {
+ accessTokenFactory: () => token,
+ })
+ .build();
+
+ this.#connection.on('notify', (payload: UmbManagementApiServerEventModel) => {
+ this.#events.next(payload);
+ });
+
+ this.#connection
+ .start()
+ .then(() => this.#isConnected.setValue(true))
+ .catch(() => this.#isConnected.setValue(false));
+
+ this.#connection.onclose(() => this.#isConnected.setValue(false));
+ }
+}
+
+export { UmbManagementApiServerEventContext as api };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/types.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/types.ts
new file mode 100644
index 0000000000..5a0341345d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/types.ts
@@ -0,0 +1,5 @@
+export interface UmbManagementApiServerEventModel {
+ eventSource: string;
+ eventType: string;
+ key: string;
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/manifests.ts
new file mode 100644
index 0000000000..c81aafcfb4
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/manifests.ts
@@ -0,0 +1,3 @@
+import { manifests as globalContextManifests } from './global-context/manifests.js';
+
+export const manifests: Array = [...globalContextManifests];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/umbraco-package.ts
new file mode 100644
index 0000000000..03e442609e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/umbraco-package.ts
@@ -0,0 +1,9 @@
+export const name = 'Umbraco.ManagementApi';
+export const extensions = [
+ {
+ name: 'Management Api Bundle',
+ alias: 'Umb.Bundle.ManagementApi',
+ type: 'bundle',
+ js: () => import('./manifests.js'),
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/vite.config.ts
new file mode 100644
index 0000000000..ff9cbc2c0d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import { rmSync } from 'fs';
+import { getDefaultConfig } from '../../vite-config-base';
+
+const dist = '../../../dist-cms/packages/management-api';
+
+// delete the unbundled dist folder
+rmSync(dist, { recursive: true, force: true });
+
+export default defineConfig({
+ ...getDefaultConfig({ dist }),
+});
diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json
index 23592d2c5b..88e278fb2d 100644
--- a/src/Umbraco.Web.UI.Client/tsconfig.json
+++ b/src/Umbraco.Web.UI.Client/tsconfig.json
@@ -14,7 +14,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"moduleDetection": "force",
"verbatimModuleSyntax": true,
"target": "es2022",
- "lib": ["es2022", "dom", "dom.iterable", "WebWorker"],
+ "lib": ["es2022", "dom", "dom.iterable"],
"outDir": "./types",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
@@ -89,6 +89,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/lit-element": ["./src/packages/core/lit-element/index.ts"],
"@umbraco-cms/backoffice/localization": ["./src/packages/core/localization/index.ts"],
"@umbraco-cms/backoffice/log-viewer": ["./src/packages/log-viewer/index.ts"],
+ "@umbraco-cms/backoffice/management-api": ["./src/packages/management-api/index.ts"],
"@umbraco-cms/backoffice/markdown-editor": ["./src/packages/markdown-editor/index.ts"],
"@umbraco-cms/backoffice/media-type": ["./src/packages/media/media-types/index.ts"],
"@umbraco-cms/backoffice/media": ["./src/packages/media/media/index.ts"],
@@ -155,6 +156,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/external/monaco-editor": ["./src/external/monaco-editor/index.ts"],
"@umbraco-cms/backoffice/external/openid": ["./src/external/openid/index.ts"],
"@umbraco-cms/backoffice/external/rxjs": ["./src/external/rxjs/index.ts"],
+ "@umbraco-cms/backoffice/external/signalr": ["./src/external/signalr/index.ts"],
"@umbraco-cms/backoffice/external/tiptap": ["./src/external/tiptap/index.ts"],
"@umbraco-cms/backoffice/external/uui": ["./src/external/uui/index.ts"]
}