diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts index 7e562cb17b..df0e30cca2 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts @@ -80,4 +80,19 @@ export class UmbExtensionRegistry { ) ) as Observable>; } + + extensionsSortedByTypeAndWeight(): Observable> { + return this.extensions.pipe( + map((exts) => exts + .sort((a, b) => { + // If type is the same, sort by weight + if (a.type === b.type) { + return (a.weight || 0) - (b.weight || 0); + } + + // Otherwise sort by type + return a.type.localeCompare(b.type); + })) + ) as Observable>; + } } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts index 97666030cd..38bc2a54bb 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts @@ -1,4 +1,5 @@ import type { ManifestBase } from './models'; +import type { UmbTreeRepositoryFactory } from '@umbraco-cms/models'; export interface ManifestTree extends ManifestBase { type: 'tree'; @@ -6,5 +7,6 @@ export interface ManifestTree extends ManifestBase { } export interface MetaTree { - storeAlias: string; + storeAlias?: string; + repository?: UmbTreeRepositoryFactory; } diff --git a/src/Umbraco.Web.UI.Client/libs/models/index.ts b/src/Umbraco.Web.UI.Client/libs/models/index.ts index 3c43ea7c17..9e91bcb3df 100644 --- a/src/Umbraco.Web.UI.Client/libs/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/models/index.ts @@ -4,7 +4,10 @@ import { DocumentTypeTreeItem, EntityTreeItem, FolderTreeItem, + PagedEntityTreeItem, + ProblemDetails, } from '@umbraco-cms/backend-api'; +import { Observable } from 'rxjs'; // Extension Manifests export * from '@umbraco-cms/extensions-registry'; @@ -149,3 +152,31 @@ export interface DocumentBlueprintDetails { icon: string; documentTypeKey: string; } + +export interface DataSourceResponse { + data?: T; + error?: ProblemDetails; +} + +// TODO; figure out why we can't add UmbControllerHostInterface as host type +export interface UmbTreeRepositoryFactory { + new (host: any): UmbTreeRepository; +} + +export interface UmbTreeRepository { + requestRootItems: () => Promise<{ + data: PagedEntityTreeItem | undefined; + error: ProblemDetails | undefined; + }>; + requestChildrenOf: (parentKey: string | null) => Promise<{ + data: PagedEntityTreeItem | undefined; + error: ProblemDetails | undefined; + }>; + requestItems: (keys: string[]) => Promise<{ + data: Array | undefined; + error: ProblemDetails | undefined; + }>; + rootItems: () => Promise>; + childrenOf: (parentKey: string | null) => Promise>; + items: (keys: string[]) => Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts b/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts index 26fbbfd9bb..fa5c50fc39 100644 --- a/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts +++ b/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts @@ -1,3 +1,4 @@ +import 'router-slot'; import { LitElement, PropertyValueMap } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { IRoute, RouterSlot } from 'router-slot'; @@ -7,6 +8,8 @@ import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/r * @element umb-router-slot-element * @description - Component for wrapping Router Slot element, providing some local events for implementation. * @extends UmbRouterSlotElement + * @fires {UmbRouterSlotInitEvent} init - fires when the media card is selected + * @fires {UmbRouterSlotChangeEvent} change - fires when the media card is unselected */ @customElement('umb-router-slot') export class UmbRouterSlotElement extends LitElement { @@ -38,8 +41,11 @@ export class UmbRouterSlotElement extends LitElement { constructor() { super(); this.#router = document.createElement('router-slot'); + // Note: I decided not to use the local changestate event, because it is not fired when the route is changed from any router-slot. And for now I wanted to keep it local. + //this.#router.addEventListener('changestate', this._onNavigationChanged); } + connectedCallback() { super.connectedCallback(); if (this.#listening === false) { @@ -54,6 +60,7 @@ export class UmbRouterSlotElement extends LitElement { this.#listening = false; } + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); this._routerPath = this.#router.constructAbsolutePath('') || ''; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dc3d008743..08608a4a56 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -28,7 +28,7 @@ "@babel/core": "^7.20.12", "@mdx-js/react": "^2.2.1", "@open-wc/testing": "^3.1.7", - "@playwright/test": "^1.29.2", + "@playwright/test": "^1.30.0", "@storybook/addon-a11y": "^6.5.15", "@storybook/addon-actions": "^6.5.14", "@storybook/addon-essentials": "^6.5.15", @@ -40,7 +40,7 @@ "@types/lodash-es": "^4.17.6", "@types/mocha": "^10.0.0", "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.48.1", "@web/dev-server-esbuild": "^0.3.3", "@web/dev-server-import-maps": "^0.0.7", @@ -66,7 +66,7 @@ "rollup": "^3.10.0", "rollup-plugin-esbuild": "^5.0.0", "tiny-glob": "^0.2.9", - "typescript": "^4.9.4", + "typescript": "^4.9.5", "vite": "^4.0.4", "vite-plugin-static-copy": "^0.13.0", "vite-tsconfig-paths": "^4.0.3", @@ -3239,13 +3239,13 @@ "dev": true }, "node_modules/@playwright/test": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.29.2.tgz", - "integrity": "sha512-+3/GPwOgcoF0xLz/opTnahel1/y42PdcgZ4hs+BZGIUjtmEFSXGg+nFoaH3NSmuc7a6GSFwXDJ5L7VXpqzigNg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.29.2" + "playwright-core": "1.30.0" }, "bin": { "playwright": "cli.js" @@ -3254,6 +3254,18 @@ "node": ">=14" } }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", @@ -6844,15 +6856,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz", - "integrity": "sha512-sR0Gja9Ky1teIq4qJOl0nC+Tk64/uYdX+mi+5iB//MH8gwyx8e3SOyhEzeLZEFEEfCaLf8KJq+Bd/6je1t+CAg==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", + "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/type-utils": "5.48.2", - "@typescript-eslint/utils": "5.48.2", + "@typescript-eslint/scope-manager": "5.50.0", + "@typescript-eslint/type-utils": "5.50.0", + "@typescript-eslint/utils": "5.50.0", "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", @@ -6876,6 +6889,53 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", + "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6954,13 +7014,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz", - "integrity": "sha512-QVWx7J5sPMRiOMJp5dYshPxABRoZV1xbRirqSk8yuIIsu0nvMTZesKErEA3Oix1k+uvsk8Cs8TGJ6kQ0ndAcew==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz", + "integrity": "sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.48.2", - "@typescript-eslint/utils": "5.48.2", + "@typescript-eslint/typescript-estree": "5.50.0", + "@typescript-eslint/utils": "5.50.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -6980,6 +7040,96 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", + "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/types": { "version": "5.48.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.2.tgz", @@ -7054,16 +7204,16 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz", - "integrity": "sha512-2h18c0d7jgkw6tdKTlNaM7wyopbLRBiit8oAxoP89YnuBOzCZ8g8aBCaCqq7h208qUTroL7Whgzam7UY3HVLow==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.50.0.tgz", + "integrity": "sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/typescript-estree": "5.48.2", + "@typescript-eslint/scope-manager": "5.50.0", + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/typescript-estree": "5.50.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -7079,6 +7229,80 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", + "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", + "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -26666,9 +26890,9 @@ } }, "node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -31328,13 +31552,21 @@ } }, "@playwright/test": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.29.2.tgz", - "integrity": "sha512-+3/GPwOgcoF0xLz/opTnahel1/y42PdcgZ4hs+BZGIUjtmEFSXGg+nFoaH3NSmuc7a6GSFwXDJ5L7VXpqzigNg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", "dev": true, "requires": { "@types/node": "*", - "playwright-core": "1.29.2" + "playwright-core": "1.30.0" + }, + "dependencies": { + "playwright-core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "dev": true + } } }, "@rollup/plugin-node-resolve": { @@ -34028,15 +34260,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz", - "integrity": "sha512-sR0Gja9Ky1teIq4qJOl0nC+Tk64/uYdX+mi+5iB//MH8gwyx8e3SOyhEzeLZEFEEfCaLf8KJq+Bd/6je1t+CAg==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", + "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/type-utils": "5.48.2", - "@typescript-eslint/utils": "5.48.2", + "@typescript-eslint/scope-manager": "5.50.0", + "@typescript-eslint/type-utils": "5.50.0", + "@typescript-eslint/utils": "5.50.0", "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", @@ -34044,6 +34277,32 @@ "tsutils": "^3.21.0" }, "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", + "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0" + } + }, + "@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -34093,15 +34352,72 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz", - "integrity": "sha512-QVWx7J5sPMRiOMJp5dYshPxABRoZV1xbRirqSk8yuIIsu0nvMTZesKErEA3Oix1k+uvsk8Cs8TGJ6kQ0ndAcew==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz", + "integrity": "sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.48.2", - "@typescript-eslint/utils": "5.48.2", + "@typescript-eslint/typescript-estree": "5.50.0", + "@typescript-eslint/utils": "5.50.0", "debug": "^4.3.4", "tsutils": "^3.21.0" + }, + "dependencies": { + "@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", + "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } } }, "@typescript-eslint/types": { @@ -34152,21 +34468,62 @@ } }, "@typescript-eslint/utils": { - "version": "5.48.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz", - "integrity": "sha512-2h18c0d7jgkw6tdKTlNaM7wyopbLRBiit8oAxoP89YnuBOzCZ8g8aBCaCqq7h208qUTroL7Whgzam7UY3HVLow==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.50.0.tgz", + "integrity": "sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.48.2", - "@typescript-eslint/types": "5.48.2", - "@typescript-eslint/typescript-estree": "5.48.2", + "@typescript-eslint/scope-manager": "5.50.0", + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/typescript-estree": "5.50.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" }, "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", + "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0" + } + }, + "@typescript-eslint/types": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", + "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", + "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "@typescript-eslint/visitor-keys": "5.50.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", + "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.50.0", + "eslint-visitor-keys": "^3.3.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -49396,9 +49753,9 @@ } }, "typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }, "typical": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 12a0cfc20e..0529dce6a7 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -76,7 +76,7 @@ "@babel/core": "^7.20.12", "@mdx-js/react": "^2.2.1", "@open-wc/testing": "^3.1.7", - "@playwright/test": "^1.29.2", + "@playwright/test": "^1.30.0", "@storybook/addon-a11y": "^6.5.15", "@storybook/addon-actions": "^6.5.14", "@storybook/addon-essentials": "^6.5.15", @@ -88,7 +88,7 @@ "@types/lodash-es": "^4.17.6", "@types/mocha": "^10.0.0", "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.48.1", "@web/dev-server-esbuild": "^0.3.3", "@web/dev-server-import-maps": "^0.0.7", @@ -114,7 +114,7 @@ "rollup": "^3.10.0", "rollup-plugin-esbuild": "^5.0.0", "tiny-glob": "^0.2.9", - "typescript": "^4.9.4", + "typescript": "^4.9.5", "vite": "^4.0.4", "vite-plugin-static-copy": "^0.13.0", "vite-tsconfig-paths": "^4.0.3", diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 1b27d767ff..4d2cd611e0 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -9,7 +9,7 @@ import '@umbraco-ui/uui-modal-container'; import '@umbraco-ui/uui-modal-dialog'; import '@umbraco-ui/uui-modal-sidebar'; import 'element-internals-polyfill'; -import 'router-slot'; +import '@umbraco-cms/router'; import type { Guard, IRoute } from 'router-slot/model'; @@ -17,6 +17,7 @@ import { UUIIconRegistryEssential } from '@umbraco-ui/uui'; import { css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; + import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { OpenAPI, RuntimeLevel, ServerResource } from '@umbraco-cms/backend-api'; @@ -151,7 +152,7 @@ export class UmbApp extends UmbLitElement { } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 5ad78a0a19..056599ad87 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -30,9 +30,10 @@ import { UmbDictionaryDetailStore } from './translation/dictionary/dictionary.de import { UmbDictionaryTreeStore } from './translation/dictionary/dictionary.tree.store'; import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store'; import { UmbDocumentBlueprintTreeStore } from './documents/document-blueprints/document-blueprint.tree.store'; - import { UmbDataTypeDetailStore } from './settings/data-types/data-type.detail.store'; -import { UmbDataTypeTreeStore } from './settings/data-types/data-type.tree.store'; +import { UmbDataTypeTreeStore } from './settings/data-types/tree/data-type.tree.store'; +import { UmbTemplateTreeStore } from './templating/templates/tree/data/template.tree.store'; +import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; import { UmbThemeContext } from './themes/theme.context'; import { UmbLanguageStore } from './settings/languages/language.store'; import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; @@ -48,6 +49,7 @@ import './translation'; import './users'; import './packages'; import './search'; +import './templating'; import './shared'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -96,6 +98,8 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbDictionaryTreeStore(this); new UmbDocumentBlueprintDetailStore(this); new UmbDocumentBlueprintTreeStore(this); + new UmbTemplateTreeStore(this); + new UmbTemplateDetailStore(this); new UmbLanguageStore(this); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/created-packages-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/created-packages-section-view.element.ts index be18117d20..24d577c5be 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/created-packages-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/created-packages-section-view.element.ts @@ -54,7 +54,7 @@ export class UmbCreatedPackagesSectionViewElement extends UmbLitElement { } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts index f1b8d80ddc..d9eced75e2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts @@ -54,7 +54,7 @@ export class UmbInstalledPackagesSectionViewElement extends UmbLitElement { } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/dashboard-examine-management.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/dashboard-examine-management.element.ts index 3ad1c32254..753b15908f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/dashboard-examine-management.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/dashboard-examine-management.element.ts @@ -6,6 +6,7 @@ import { UmbDashboardExamineIndexElement } from './views/section-view-examine-in import { UmbDashboardExamineSearcherElement } from './views/section-view-examine-searchers'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/router'; @customElement('umb-dashboard-examine-management') export class UmbDashboardExamineManagementElement extends UmbLitElement { @@ -46,28 +47,24 @@ export class UmbDashboardExamineManagementElement extends UmbLitElement { ]; @state() - private _currentPath?: string; + private _routerPath?: string; - /** - * - */ - constructor() { - super(); - } + @state() + private _activePath = ''; - private _onRouteChange() { - this._currentPath = path(); - } - - private get backbutton(): boolean { - return !(this._currentPath?.endsWith('examine-management/')); - } render() { - return html` ${this.backbutton - ? html` ← Back to overview ` + return html` ${this._routerPath && this._activePath !== '' + ? html` ← Back to overview ` : nothing} - `; + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath || ''; + }}>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts index 91fb021085..93ea82a6f0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/health-check/dashboard-health-check.element.ts @@ -76,7 +76,7 @@ export class UmbDashboardHealthCheckElement extends UmbLitElement { } render() { - return html` `; + return html` `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/data-type.tree.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.tree.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/data-type.tree.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts index a73a6c4236..d4467cddb1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts @@ -1,11 +1,10 @@ -import { UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from '../data-type.tree.store'; +import { UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './data-type.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.DataTypes', name: 'Data Types Tree', - weight: 100, meta: { storeAlias: UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString(), }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts index 26bcf43f2a..563af4962a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts @@ -1,33 +1,55 @@ import { html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { isManifestElementNameType , umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +import { isManifestElementNameType, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import type { ManifestBase } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; @customElement('umb-extension-root-workspace') export class UmbExtensionRootWorkspaceElement extends UmbLitElement { @state() private _extensions?: Array = undefined; + private _modalService?: UmbModalService; + connectedCallback(): void { super.connectedCallback(); this._observeExtensions(); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (modalService) => { + this._modalService = modalService; + }); } private _observeExtensions() { - this.observe(umbExtensionsRegistry.extensions, (extensions) => { + this.observe(umbExtensionsRegistry.extensionsSortedByTypeAndWeight(), (extensions) => { this._extensions = extensions || undefined; }); } + #removeExtension(extension: ManifestBase) { + const modalHandler = this._modalService?.confirm({ + headline: 'Unload extension', + confirmLabel: 'Unload', + content: html`

Are you sure you want to unload the extension ${extension.alias}?

`, + color: 'danger', + }); + + modalHandler?.onClose().then(({ confirmed }: any) => { + if (confirmed) { + umbExtensionsRegistry.unregister(extension.alias); + } + }); + } + render() { return html` -

List of currently loaded extensions

Type + Weight Name Alias Actions @@ -37,14 +59,19 @@ export class UmbExtensionRootWorkspaceElement extends UmbLitElement { (extension) => html` ${extension.type} + ${extension.weight ? extension.weight : 'Not Set'} - ${isManifestElementNameType(extension) ? extension.name : 'Custom extension'} + ${isManifestElementNameType(extension) ? extension.name : `[Custom extension] ${extension.name}`} ${extension.alias} umbExtensionsRegistry.unregister(extension.alias)}> + label="Unload" + color="danger" + look="primary" + @click=${() => this.#removeExtension(extension)}> + + ` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts index d9a52420ca..81e529e706 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts @@ -103,7 +103,7 @@ export class UmbCollectionElement extends UmbLitElement { return html` - + ${this._selection && this._selection.length > 0 ? html`` : nothing} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts index e2c04a8cfe..1bc842424b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts @@ -2,12 +2,12 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { state } from 'lit/decorators.js'; -import { IRoutingInfo } from 'router-slot'; import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section/section.context'; import { UmbBackofficeContext, UMB_BACKOFFICE_CONTEXT_TOKEN } from './backoffice.context'; import type { ManifestSection } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; import { createExtensionElementOrFallback } from '@umbraco-cms/extensions-api'; +import { UmbRouterSlotChangeEvent } from '@umbraco-cms/router'; @defineElement('umb-backoffice-main') export class UmbBackofficeMain extends UmbLitElement { @@ -67,9 +67,6 @@ export class UmbBackofficeMain extends UmbLitElement { return { path: this._routePrefix + section.meta.pathname, component: () => createExtensionElementOrFallback(section, 'umb-section'), - setup: this._onRouteSetup, - // TODO: sometimes we can end up in a state where this callback doesn't get called. It could look like a bug in the router-slot. - // Niels: Could this be because _backofficeContext is not available at that state? }; }); @@ -79,8 +76,8 @@ export class UmbBackofficeMain extends UmbLitElement { }); } - private _onRouteSetup = (_component: HTMLElement, info: IRoutingInfo) => { - const currentPath = info.match.route.path; + private _onRouteChange = (event: UmbRouterSlotChangeEvent) => { + const currentPath = event.target.localActiveViewPath || '' const section = this._sections.find((s) => this._routePrefix + s.meta.pathname === currentPath); if (!section) return; this._backofficeContext?.setActiveSectionAlias(section.alias); @@ -97,7 +94,11 @@ export class UmbBackofficeMain extends UmbLitElement { } render() { - return html``; + return html` + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.element.ts new file mode 100644 index 0000000000..e33d98d274 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.element.ts @@ -0,0 +1,66 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +@customElement('umb-empty-state') +export class UmbEmptyStateElement extends LitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + text-align: center; + padding: var(--uui-size-space-4); + } + + :host([position='center']) { + position: absolute; + top: 50%; + left: 50%; + max-width: 400px; + width: 80%; + transform: translate(-50%, -50%); + } + + :host(:not([position='center'])) { + margin: auto; + } + + :host(:not([size='small'])) { + font-size: var(--uui-size-6); + } + + :host([size='small']) { + font-size: var(--uui-size-5); + } + + slot { + margin: auto; + } + `, + ]; + + /** + * Set the text size + */ + @property({ type: String }) + size: 'small' | 'large' = 'large'; + + /** + * Set the element position + * 'center' => element is absolutely centered + * undefined => element has auto margin, to center in parent + */ + @property({ type: String }) + position: 'center' | undefined; + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-empty-state': UmbEmptyStateElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 717317aa34..470ab00cac 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -16,3 +16,5 @@ import './tree/tree.element'; import './workspace/workspace-content/workspace-content.element'; import './input-media-picker/input-media-picker.element'; import './input-document-picker/input-document-picker.element'; +import './empty-state/empty-state.element'; + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts index 90379166a8..c3f2d1f68d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts @@ -8,6 +8,7 @@ import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/exte import type { ManifestDashboard, ManifestDashboardCollection, ManifestWithMeta } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/router'; @customElement('umb-section-dashboards') export class UmbSectionDashboardsElement extends UmbLitElement { @@ -27,7 +28,8 @@ export class UmbSectionDashboardsElement extends UmbLitElement { } #scroll-container { - flex: 1; + flex:1; + position:relative; } #router-slot { @@ -41,14 +43,14 @@ export class UmbSectionDashboardsElement extends UmbLitElement { @state() private _dashboards?: Array; - @state() - private _currentDashboardPathname = ''; - @state() private _routes: Array = []; @state() - private _currentSectionPathname = ''; + private _routerPath?: string; + + @state() + private _activePath?: string; private _currentSectionAlias?: string; private _sectionContext?: UmbSectionContext; @@ -69,9 +71,6 @@ export class UmbSectionDashboardsElement extends UmbLitElement { this._currentSectionAlias = alias; this._observeDashboards(); }); - this.observe(this._sectionContext.pathname.pipe(first()), (pathname) => { - this._currentSectionPathname = pathname || ''; - }); } private _observeDashboards() { @@ -108,7 +107,6 @@ export class UmbSectionDashboardsElement extends UmbLitElement { return createExtensionElement(dashboard); }, setup: (component: Promise | HTMLElement, info: IRoutingInfo) => { - this._currentDashboardPathname = info.match.route.path; // When its using import, we get an element, when using createExtensionElement we get a Promise. // TODO: this is a bit hacky, can we do it in a more appropriate way: if ((component as any).then) { @@ -135,9 +133,9 @@ export class UmbSectionDashboardsElement extends UmbLitElement { ${this._dashboards.map( (dashboard) => html` + ?active="${dashboard.meta.pathname === this._activePath}"> ` )} @@ -150,7 +148,16 @@ export class UmbSectionDashboardsElement extends UmbLitElement { return html` ${this._renderNavigation()} - + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }} + > `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts index b4565ccee2..6c63ea29ae 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts @@ -11,6 +11,7 @@ import { UmbLitElement } from '@umbraco-cms/element'; import './section-sidebar-menu/section-sidebar-menu.element.ts'; import './section-views/section-views.element.ts'; +import { UmbRouterSlotChangeEvent } from '@umbraco-cms/router'; @customElement('umb-section') export class UmbSectionElement extends UmbLitElement { @@ -173,9 +174,6 @@ export class UmbSectionElement extends UmbLitElement { return { path: 'view/' + view.meta.pathname, component: () => createExtensionElement(view), - setup: () => { - this._sectionContext?.setActiveView(view); - }, }; }) ?? []; @@ -187,6 +185,13 @@ export class UmbSectionElement extends UmbLitElement { } } + private _onRouteChange = (event: UmbRouterSlotChangeEvent) => { + const currentPath = event.target.localActiveViewPath; + const view = this._views?.find((view) => 'view/' + view.meta.pathname === currentPath); + if (!view) return; + this._sectionContext?.setActiveView(view); + } + render() { return html` ${this._menuItems && this._menuItems.length > 0 @@ -199,7 +204,7 @@ export class UmbSectionElement extends UmbLitElement { ${this._views && this._views.length > 0 ? html`` : nothing} ${this._routes && this._routes.length > 0 - ? html`` + ? html`` : nothing} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts index 6ee43fcd8d..1f611ae50c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts @@ -167,8 +167,21 @@ export class UmbTreeItem extends UmbLitElement { private _onShowChildren() { if (this._childItems && this._childItems.length > 0) return; this._observeChildren(); + this._observeRepositoryChildren(); } + private async _observeRepositoryChildren() { + if (!this._treeContext?.requestChildrenOf) return; + + // TODO: add loading state + this._treeContext.requestChildrenOf(this.key); + + this.observe(await this._treeContext.childrenOf(this.key), (childItems) => { + this._childItems = childItems as Entity[]; + }); + } + + // TODO: remove when repositories are in place private _observeChildren() { if (!this._store?.getTreeItemChildren) return; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts index 45a04dec82..28b0dda2ba 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts @@ -1,6 +1,7 @@ import type { Observable } from 'rxjs'; -import type { ManifestTree } from '@umbraco-cms/models'; +import type { ManifestTree, UmbTreeRepository } from '@umbraco-cms/models'; import { DeepState } from '@umbraco-cms/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; export interface UmbTreeContext { tree: ManifestTree; @@ -12,6 +13,7 @@ export interface UmbTreeContext { } export class UmbTreeContextBase implements UmbTreeContext { + #host: UmbControllerHostInterface; public tree: ManifestTree; #selectable = new DeepState(false); @@ -20,8 +22,15 @@ export class UmbTreeContextBase implements UmbTreeContext { #selection = new DeepState(>[]); public readonly selection = this.#selection.asObservable(); - constructor(tree: ManifestTree) { + repository!: UmbTreeRepository; + + constructor(host: UmbControllerHostInterface, tree: ManifestTree) { + this.#host = host; this.tree = tree; + + if (this.tree.meta.repository) { + this.repository = new this.tree.meta.repository(this.#host); + } } public setSelectable(value: boolean) { @@ -35,7 +44,7 @@ export class UmbTreeContextBase implements UmbTreeContext { public select(key: string) { const oldSelection = this.#selection.getValue(); - if(oldSelection.indexOf(key) !== -1) return; + if (oldSelection.indexOf(key) !== -1) return; const selection = [...oldSelection, key]; this.#selection.next(selection); @@ -45,4 +54,20 @@ export class UmbTreeContextBase implements UmbTreeContext { const selection = this.#selection.getValue(); this.#selection.next(selection.filter((x) => x !== key)); } + + public async requestRootItems() { + return this.repository.requestRootItems(); + } + + public async requestChildrenOf(parentKey: string | null) { + return this.repository.requestChildrenOf(parentKey); + } + + public async rootItems() { + return this.repository.rootItems(); + } + + public async childrenOf(parentKey: string | null) { + return this.repository.childrenOf(parentKey); + } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts index bfce7849ae..7fba991109 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts @@ -64,8 +64,9 @@ export class UmbTreeElement extends UmbLitElement { private _treeContext?: UmbTreeContextBase; private _store?: UmbTreeStore; - connectedCallback(): void { - super.connectedCallback(); + #treeRepository?: any; // TODO: make interface + + protected firstUpdated(): void { this._observeTree(); } @@ -76,12 +77,15 @@ export class UmbTreeElement extends UmbLitElement { umbExtensionsRegistry .extensionsOfType('tree') .pipe(map((trees) => trees.find((tree) => tree.alias === this.alias))), - (tree) => { + async (tree) => { + if (this._tree?.alias === tree?.alias) return; + this._tree = tree; - if (tree) { - this._provideTreeContext(); + this._provideTreeContext(); + + // TODO: remove this when repositories are in place. + if (this._tree?.meta.storeAlias) { this._provideStore(); - this._observeTreeRoot(); } } ); @@ -92,15 +96,17 @@ export class UmbTreeElement extends UmbLitElement { // TODO: if a new tree comes around, which is different, then we should clean up and re provide. - this._treeContext = new UmbTreeContextBase(this._tree); + this._treeContext = new UmbTreeContextBase(this, this._tree); this._treeContext.setSelectable(this.selectable); this._treeContext.setSelection(this.selection); this._observeSelection(); + this._observeRepositoryTreeRoot(); this.provideContext('umbTreeContext', this._treeContext); } + // TODO: remove this when repositories are in place. private _provideStore() { // TODO: Clean up store, if already existing. @@ -109,6 +115,17 @@ export class UmbTreeElement extends UmbLitElement { this.consumeContext(this._tree.meta.storeAlias, (store: UmbTreeStore) => { this._store = store; this.provideContext('umbStore', store); + this._observeStoreTreeRoot(); + }); + } + + private async _observeRepositoryTreeRoot() { + if (!this._treeContext?.requestRootItems) return; + + this._treeContext.requestRootItems(); + + this.observe(await this._treeContext.rootItems(), (rootItems) => { + this._items = rootItems as Entity[]; }); } @@ -122,7 +139,7 @@ export class UmbTreeElement extends UmbLitElement { }); } - private _observeTreeRoot() { + private _observeStoreTreeRoot() { if (!this._store?.getTreeRoot) return; this._loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts new file mode 100644 index 0000000000..5fa6ac29c3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts @@ -0,0 +1,12 @@ +import { manifests as templateManifests } from './templates/manifests'; +import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +import { ManifestTypes } from '@umbraco-cms/extensions-registry'; + +const registerExtensions = (manifests: Array) => { + manifests.forEach((manifest) => { + if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; + umbExtensionsRegistry.register(manifest); + }); +}; + +registerExtensions([...templateManifests]); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/manifests.ts new file mode 100644 index 0000000000..a4edc8b4f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests'; +import { manifests as treeManifests } from './tree/manifests'; +import { manifests as workspaceManifests } from './workspace/manifests'; + +export const manifests = [...sidebarMenuItemManifests, ...treeManifests, ...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/manifests.ts new file mode 100644 index 0000000000..8785234af7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/manifests.ts @@ -0,0 +1,17 @@ +import type { ManifestSidebarMenuItem } from '@umbraco-cms/models'; + +const sidebarMenuItem: ManifestSidebarMenuItem = { + type: 'sidebarMenuItem', + alias: 'Umb.SidebarMenuItem.Templates', + name: 'Templates Sidebar Menu Item', + weight: 40, + loader: () => import('./templates-sidebar-menu-item.element'), + meta: { + label: 'Templates', + icon: 'umb:folder', + sections: ['Umb.Section.Settings'], + entityType: 'template', + }, +}; + +export const manifests = [sidebarMenuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/templates-sidebar-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/templates-sidebar-menu-item.element.ts new file mode 100644 index 0000000000..866b1e58b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/sidebar-menu-item/templates-sidebar-menu-item.element.ts @@ -0,0 +1,40 @@ +import { html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-templates-sidebar-menu-item') +export class UmbTemplatesSidebarMenuItemElement extends UmbLitElement { + @state() + private _renderTree = false; + + private _onShowChildren() { + this._renderTree = true; + } + + private _onHideChildren() { + this._renderTree = false; + } + + // TODO: check if root has children before settings the has-children attribute + // TODO: how do we want to cache the tree? (do we want to rerender every time the user opens the tree)? + // TODO: can we make this reusable? + render() { + return html` + ${this._renderTree ? html`` : nothing} + `; + } +} + +export default UmbTemplatesSidebarMenuItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-templates-sidebar-menu-item': UmbTemplatesSidebarMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/create/create-template-tree-action.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/create/create-template-tree-action.element.ts new file mode 100644 index 0000000000..12ac8a21e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/create/create-template-tree-action.element.ts @@ -0,0 +1,34 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import UmbTreeItemActionElement from '../../../../../shared/components/tree/action/tree-item-action.element'; + +@customElement('umb-create-template-tree-action') +export default class UmbCreateTemplateTreeAction extends UmbTreeItemActionElement { + static styles = [UUITextStyles, css``]; + + // TODO: how do we handle the href? + private _constructUrl() { + return `section/settings/${this._activeTreeItem?.type}/create/${this._activeTreeItem?.key || 'root'}`; + } + + private _handleLabelClick() { + if (!this._treeContextMenuService) return; + this._treeContextMenuService.close(); + } + + render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-create-template-tree-action': UmbCreateTemplateTreeAction; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/delete/delete-template-tree-action.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/delete/delete-template-tree-action.element.ts new file mode 100644 index 0000000000..19a9ae8afe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/actions/delete/delete-template-tree-action.element.ts @@ -0,0 +1,31 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import UmbTreeItemActionElement from '../../../../../shared/components/tree/action/tree-item-action.element'; +import { UmbTemplateDetailRepository } from '../../../workspace/data/template.detail.repository'; + +@customElement('umb-delete-template-tree-action') +export default class UmbDeleteTemplateTreeAction extends UmbTreeItemActionElement { + static styles = [UUITextStyles, css``]; + #templateDetailRepo = new UmbTemplateDetailRepository(this); + + private _handleLabelClick() { + if (!this._activeTreeItem?.key) return; + if (!this._treeContextMenuService) return; + + this.#templateDetailRepo.delete(this._activeTreeItem.key); + this._treeContextMenuService.close(); + } + + render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-delete-template-tree-action': UmbDeleteTemplateTreeAction; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/index.ts new file mode 100644 index 0000000000..b1d7c01021 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/index.ts @@ -0,0 +1,8 @@ +import type { DataSourceResponse } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; + +export interface TemplateTreeDataSource { + getRootItems(): Promise>; + getChildrenOf(parentKey: string): Promise>; + getItems(key: Array): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/template.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/template.tree.server.data.ts new file mode 100644 index 0000000000..9378d45c85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/template.tree.server.data.ts @@ -0,0 +1,72 @@ +import { TemplateTreeDataSource } from '.'; +import { ProblemDetails, TemplateResource } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Template tree that fetches data from the server + * @export + * @class TemplateTreeServerDataSource + * @implements {TemplateTreeDataSource} + */ +export class TemplateTreeServerDataSource implements TemplateTreeDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of TemplateTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof TemplateTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof TemplateTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, TemplateResource.getTreeTemplateRoot({})); + } + + /** + * Fetches the children of a given parent key from the server + * @param {(string | null)} parentKey + * @return {*} + * @memberof TemplateTreeServerDataSource + */ + async getChildrenOf(parentKey: string | null) { + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + TemplateResource.getTreeTemplateChildren({ + parentKey, + }) + ); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof TemplateTreeServerDataSource + */ + async getItems(keys: Array) { + if (keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + TemplateResource.getTreeTemplateItem({ + key: keys, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.repository.ts new file mode 100644 index 0000000000..eb786f8f09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.repository.ts @@ -0,0 +1,104 @@ +import { TemplateTreeServerDataSource } from './sources/template.tree.server.data'; +import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from './template.tree.store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import type { UmbTreeRepository } from '@umbraco-cms/models'; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbTemplateTreeRepository implements UmbTreeRepository { + #host: UmbControllerHostInterface; + #dataSource: TemplateTreeServerDataSource; + #treeStore?: UmbTemplateTreeStore; + #notificationService?: UmbNotificationService; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + // TODO: figure out how spin up get the correct data source + this.#dataSource = new TemplateTreeServerDataSource(this.#host); + + new UmbContextConsumerController(this.#host, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + this.#checkIfInitialized(); + }); + } + + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + + #checkIfInitialized() { + if (this.#treeStore && this.#notificationService) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async requestRootItems() { + await this.#init; + + const { data, error } = await this.#dataSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error }; + } + + async requestChildrenOf(parentKey: string | null) { + await this.#init; + + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#dataSource.getChildrenOf(parentKey); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error }; + } + + async requestItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#dataSource.getItems(keys); + + return { data, error }; + } + + async rootItems() { + await this.#init; + return this.#treeStore!.rootItems(); + } + + async childrenOf(parentKey: string | null) { + await this.#init; + return this.#treeStore!.childrenOf(parentKey); + } + + async items(keys: Array) { + await this.#init; + return this.#treeStore!.items(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.store.ts new file mode 100644 index 0000000000..c9bbaaafba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.store.ts @@ -0,0 +1,95 @@ +import { EntityTreeItem } from '@umbraco-cms/backend-api'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbTemplateTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Templates + */ +export class UmbTemplateTreeStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbTemplateTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbTemplateTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbTemplateTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} key + * @param {Partial} data + * @memberof UmbTemplateTreeStore + */ + updateItem(key: string, data: Partial) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.appendOne({ ...entry, ...data }); + } + } + + /** + * Removes an item from the store + * @param {string} key + * @memberof UmbTemplateTreeStore + */ + removeItem(key: string) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.remove([key]); + } + } + + /** + * Returns an observable to observe the root items + * @return {*} + * @memberof UmbTemplateTreeStore + */ + rootItems() { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); + } + + /** + * Returns an observable to observe the children of a given parent + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbTemplateTreeStore + */ + childrenOf(parentKey: string | null) { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); + } + + /** + * Returns an observable to observe the items with the given keys + * @param {Array} keys + * @return {*} + * @memberof UmbTemplateTreeStore + */ + items(keys: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} + +export const UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbTemplateTreeStore.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/manifests.ts new file mode 100644 index 0000000000..8a75fcc7a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/manifests.ts @@ -0,0 +1,40 @@ +import { UmbTemplateTreeRepository } from './data/template.tree.repository'; +import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; + +const tree: ManifestTree = { + type: 'tree', + alias: 'Umb.Tree.Templates', + name: 'Templates Tree', + meta: { + repository: UmbTemplateTreeRepository, + }, +}; + +const treeItemActions: Array = [ + { + type: 'treeItemAction', + alias: 'Umb.TreeItemAction.Template.Create', + name: 'Create Template Tree Action', + loader: () => import('./actions/create/create-template-tree-action.element'), + weight: 300, + meta: { + entityType: 'template', + label: 'Create', + icon: 'umb:add', + }, + }, + { + type: 'treeItemAction', + alias: 'Umb.TreeItemAction.Template.Delete', + name: 'Delete Template Tree Action', + loader: () => import('./actions/delete/delete-template-tree-action.element'), + weight: 200, + meta: { + entityType: 'template', + label: 'Delete', + icon: 'umb:trash', + }, + }, +]; + +export const manifests = [tree, ...treeItemActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts new file mode 100644 index 0000000000..bfea9e5977 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts @@ -0,0 +1,10 @@ +import { Template } from '@umbraco-cms/backend-api'; +import type { DataSourceResponse } from '@umbraco-cms/models'; + +export interface TemplateDetailDataSource { + createScaffold(parentKey: string | null): Promise>; + get(key: string): Promise>; + insert(template: Template): Promise; + update(template: Template): Promise; + delete(key: string): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts new file mode 100644 index 0000000000..50b2781d6d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts @@ -0,0 +1,113 @@ +import { v4 as uuid } from 'uuid'; +import { TemplateDetailDataSource } from '.'; +import { ProblemDetails, Template, TemplateResource } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Template detail that fetches data from the server + * @export + * @class UmbTemplateDetailServerDataSource + * @implements {TemplateDetailDataSource} + */ +export class UmbTemplateDetailServerDataSource implements TemplateDetailDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbTemplateDetailServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbTemplateDetailServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches a Template with the given key from the server + * @param {string} key + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + get(key: string) { + return tryExecuteAndNotify(this.#host, TemplateResource.getTemplateByKey({ key })); + } + + /** + * Creates a new Template scaffold + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + async createScaffold(parentKey: string | null) { + let masterTemplateAlias: string | undefined = undefined; + let error = undefined; + const data: Template = { + key: uuid(), + name: '', + alias: '', + content: '', + }; + + // TODO: update when backend is updated so we don't have to do two calls + if (parentKey) { + const { data: parentData, error: parentError } = await tryExecuteAndNotify( + this.#host, + TemplateResource.getTemplateByKey({ key: parentKey }) + ); + masterTemplateAlias = parentData?.alias; + error = parentError; + } + + const { data: scaffoldData, error: scaffoldError } = await tryExecuteAndNotify( + this.#host, + TemplateResource.getTemplateScaffold({ masterTemplateAlias }) + ); + + error = scaffoldError; + data.content = scaffoldData?.content || ''; + + return { data, error }; + } + + /** + * Inserts a new Template on the server + * @param {Template} template + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + async insert(template: Template) { + const payload = { requestBody: template }; + return tryExecuteAndNotify(this.#host, TemplateResource.postTemplate(payload)); + } + + /** + * Updates a Template on the server + * @param {Template} template + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + async update(template: Template) { + if (!template.key) { + const error: ProblemDetails = { title: 'Template key is missing' }; + return { error }; + } + + const payload = { key: template.key, requestBody: template }; + return tryExecuteAndNotify(this.#host, TemplateResource.putTemplateByKey(payload)); + } + + /** + * Deletes a Template on the server + * @param {string} key + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + async delete(key: string) { + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + return await tryExecuteAndNotify(this.#host, TemplateResource.deleteTemplateByKey({ key })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts new file mode 100644 index 0000000000..995076b1ac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts @@ -0,0 +1,163 @@ +import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from '../../tree/data/template.tree.store'; +import { UmbTemplateDetailStore, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN } from './template.detail.store'; +import { UmbTemplateDetailServerDataSource } from './sources/template.detail.server.data'; +import { ProblemDetails, Template } from '@umbraco-cms/backend-api'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbTemplateDetailRepository { + + #host: UmbControllerHostInterface; + #dataSource: UmbTemplateDetailServerDataSource; + #detailStore?: UmbTemplateDetailStore; + #treeStore?: UmbTemplateTreeStore; + #notificationService?: UmbNotificationService; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + // TODO: figure out how spin up get the correct data source + this.#dataSource = new UmbTemplateDetailServerDataSource(this.#host); + + // TODO: should we allow promises so each method can request the context when it needs it instead of initializing it upfront? + new UmbContextConsumerController(this.#host, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { + this.#detailStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + this.#checkIfInitialized(); + }); + } + + #init() { + // TODO: This would only works with one user of this method. If two, the first one would be forgotten, but maybe its alright for now as I guess this is temporary. + return new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + } + + #checkIfInitialized() { + if (this.#detailStore && this.#detailStore && this.#notificationService) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async createScaffold(parentKey: string | null) { + await this.#init(); + + // TODO: should we show a notification if the parent key is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { data: undefined, error }; + } + + return this.#dataSource.createScaffold(parentKey); + } + + async get(key: string) { + await this.#init(); + + // TODO: should we show a notification if the key is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + return this.#dataSource.get(key); + } + + async insert(template: Template) { + await this.#init(); + + // TODO: should we show a notification if the template is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!template) { + const error: ProblemDetails = { title: 'Template is missing' }; + return { error }; + } + + const { error } = await this.#dataSource.insert(template); + + if (!error) { + const notification = { data: { message: `Template created` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server + this.#detailStore?.append(template); + // TODO: Update tree store with the new item? + + return { error }; + } + + async update(template: Template) { + await this.#init(); + + // TODO: should we show a notification if the template is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!template || !template.key) { + const error: ProblemDetails = { title: 'Template is missing' }; + return { error }; + } + + const { error } = await this.#dataSource.update(template); + + if (!error) { + const notification = { data: { message: `Template saved` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server + // Consider notify a workspace if a template is updated in the store while someone is editing it. + this.#detailStore?.append(template); + this.#treeStore?.updateItem(template.key, { name: template.name }); + // TODO: would be nice to align the stores on methods/methodNames. + + return { error }; + } + + async delete(key: string) { + await this.#init(); + + // TODO: should we show a notification if the key is missing? + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + const { error } = await this.#dataSource.delete(key); + + if (!error) { + const notification = { data: { message: `Template deleted` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server. + // Consider notify a workspace if a template is deleted from the store while someone is editing it. + this.#detailStore?.remove([key]); + this.#treeStore?.removeItem(key); + // TODO: would be nice to align the stores on methods/methodNames. + + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts new file mode 100644 index 0000000000..617c043d70 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts @@ -0,0 +1,46 @@ +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { Template } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbTemplateDetailStore + * @extends {UmbStoreBase} + * @description - Data Store for Template Details + */ +export class UmbTemplateDetailStore extends UmbStoreBase { + #data = new ArrayState