diff --git a/src/Umbraco.Web.UI.Client/.vscode/settings.json b/src/Umbraco.Web.UI.Client/.vscode/settings.json index 196b1d2b83..badd3c2f00 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/settings.json +++ b/src/Umbraco.Web.UI.Client/.vscode/settings.json @@ -1,6 +1,6 @@ { "cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"], - "cSpell.words": ["combobox", "variantable"], + "cSpell.words": ["combobox", "templating", "variantable"], "exportall.config.folderListener": [], "exportall.config.relExclusion": [] } diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts index 2d30084d7f..f9b72588b2 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '../context-token'; +import { UmbContextToken } from '../token/context-token'; import { UmbContextConsumer } from './context-consumer'; import { UmbContextCallback } from './context-request.event'; import type { UmbControllerHostInterface, UmbControllerInterface } from '@umbraco-cms/backoffice/controller'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts index 4f725c4767..32bc1781a8 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '../context-token'; +import { UmbContextToken } from '../token/context-token'; import { isUmbContextProvideEventType, umbContextProvideEventType } from '../provide/context-provide.event'; import { UmbContextRequestEventImplementation, UmbContextCallback } from './context-request.event'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts index ab96c5bd51..d1b630be4b 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '../context-token'; +import { UmbContextToken } from '../token/context-token'; export const umbContextRequestEventType = 'umb:context-request'; export const umbDebugContextEventType = 'umb:debug-contexts'; @@ -33,9 +33,8 @@ export const isUmbContextRequestEvent = (event: Event): event is UmbContextReque return event.type === umbContextRequestEventType; }; - export class UmbContextDebugRequest extends Event { - public constructor(public readonly callback:any) { + public constructor(public readonly callback: any) { super(umbDebugContextEventType, { bubbles: true, composed: true, cancelable: false }); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/index.ts b/src/Umbraco.Web.UI.Client/libs/context-api/index.ts index 67aaa8edcf..39b438786a 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/index.ts @@ -4,4 +4,4 @@ export * from './consume/context-request.event'; export * from './provide/context-provider.controller'; export * from './provide/context-provider'; export * from './provide/context-provide.event'; -export * from './context-token'; +export * from './token/context-token'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provide.event.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provide.event.ts index 37b88cea24..9388743921 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provide.event.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provide.event.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '../context-token'; +import { UmbContextToken } from '../token/context-token'; export const umbContextProvideEventType = 'umb:context-provide'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.ts index 54c9e2e167..7412ae6a0c 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '../context-token'; +import { UmbContextToken } from '../token/context-token'; import { UmbContextProvider } from './context-provider'; import type { UmbControllerHostInterface, UmbControllerInterface } from '@umbraco-cms/backoffice/controller'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts index c9ce3ab9c3..efed204d3a 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts @@ -1,5 +1,9 @@ -import { umbContextRequestEventType, isUmbContextRequestEvent, umbDebugContextEventType } from '../consume/context-request.event'; -import { UmbContextToken } from '../context-token'; +import { + umbContextRequestEventType, + isUmbContextRequestEvent, + umbDebugContextEventType, +} from '../consume/context-request.event'; +import { UmbContextToken } from '../token/context-token'; import { UmbContextProvideEventImplementation } from './context-provide.event'; /** @@ -68,14 +72,14 @@ export class UmbContextProvider { private _handleDebugContextRequest = (event: any) => { // If the event doesn't have an instances property, create it. - if(!event.instances){ + if (!event.instances) { event.instances = new Map(); } // If the event doesn't have an instance for this context, add it. // Nearest to the DOM element of will be added first // as contexts can change/override deeper in the DOM - if(!event.instances.has(this._contextAlias)){ + if (!event.instances.has(this._contextAlias)) { event.instances.set(this._contextAlias, this.#instance); } }; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/context-token.test.ts b/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/libs/context-api/context-token.test.ts rename to src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts index 66531d06f0..f8b0a2046d 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/context-token.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts @@ -1,7 +1,7 @@ import { expect } from '@open-wc/testing'; -import { UmbContextConsumer } from './consume/context-consumer'; +import { UmbContextConsumer } from '../consume/context-consumer'; +import { UmbContextProvider } from '../provide/context-provider'; import { UmbContextToken } from './context-token'; -import { UmbContextProvider } from './provide/context-provider'; const testContextAlias = 'my-test-context'; diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/context-token.ts b/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/libs/context-api/context-token.ts rename to src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.ts 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 c380dc602e..c1279b91f7 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 @@ -47,7 +47,7 @@ export class UmbExtensionRegistry { const nextData = this._kinds .getValue() .filter( - (k) => k.matchType !== (kind as ManifestKind).matchType && k.matchKind !== (kind as ManifestKind).matchKind + (k) => !(k.matchType === (kind as ManifestKind).matchType && k.matchKind === (kind as ManifestKind).matchKind) ); nextData.push(kind as ManifestKind); this._kinds.next(nextData); @@ -125,7 +125,10 @@ export class UmbExtensionRegistry { T extends ManifestBase = SpecificManifestTypeOrManifestBase >(type: Key, alias: string) { return combineLatest([ - this.extensions.pipe(map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias))), + this.extensions.pipe( + map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias)), + distinctUntilChanged(extensionSingleMemoization) + ), this._kindsOfType(type), ]).pipe( map(([ext, kinds]) => { diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts index 871567b74d..615b0db9d1 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts @@ -16,6 +16,7 @@ import type { ManifestMenu } from './menu.models'; import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.models'; import type { ManifestTheme } from './theme.models'; import type { ManifestTree } from './tree.models'; +import type { ManifestTreeItem } from './tree-item.models'; import type { ManifestUserDashboard } from './user-dashboard.models'; import type { ManifestWorkspace } from './workspace.models'; import type { ManifestWorkspaceAction } from './workspace-action.models'; @@ -44,6 +45,7 @@ export * from './menu.models'; export * from './menu-item.models'; export * from './theme.models'; export * from './tree.models'; +export * from './tree-item.models'; export * from './user-dashboard.models'; export * from './workspace-action.models'; export * from './workspace-view-collection.models'; @@ -78,6 +80,7 @@ export type ManifestTypes = | ManifestMenuItemTreeKind | ManifestTheme | ManifestTree + | ManifestTreeItem | ManifestUserDashboard | ManifestWorkspace | ManifestWorkspaceAction diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts index abeb6b9fda..18e8c10c17 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts @@ -1,10 +1,10 @@ import type { ManifestClass } from './models'; -import { UmbStoreBase, UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; export interface ManifestStore extends ManifestClass { type: 'store'; } -export interface ManifestTreeStore extends ManifestClass { +export interface ManifestTreeStore extends ManifestClass { type: 'treeStore'; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree-item.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree-item.models.ts new file mode 100644 index 0000000000..9231a8ae68 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree-item.models.ts @@ -0,0 +1,10 @@ +import type { ManifestElement } from './models'; + +export interface ManifestTreeItem extends ManifestElement { + type: 'treeItem'; + conditions: ConditionsTreeItem; +} + +export interface ConditionsTreeItem { + entityType: string; +} 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 d4eea123a9..49e8387bb4 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,5 +1,4 @@ import type { ManifestBase } from './models'; -import type { ClassConstructor } from '@umbraco-cms/backoffice/models'; export interface ManifestTree extends ManifestBase { type: 'tree'; @@ -7,6 +6,5 @@ export interface ManifestTree extends ManifestBase { } export interface MetaTree { - storeAlias?: string; - repository?: ClassConstructor; + repositoryAlias: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source-response.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/libs/repository/data-source-response.interface.ts rename to src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts new file mode 100644 index 0000000000..766ddf1e87 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts @@ -0,0 +1,9 @@ +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbDataSource { + createScaffold(parentKey: string | null): Promise>; + get(key: string): Promise>; + insert(data: T): Promise>; + update(data: T): Promise>; + delete(key: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts new file mode 100644 index 0000000000..0c12e00d34 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts @@ -0,0 +1,3 @@ +export * from './data-source-response.interface'; +export * from './data-source.interface'; +export * from './tree-data-source.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts new file mode 100644 index 0000000000..ae57ef57af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts @@ -0,0 +1,7 @@ +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbTreeDataSource { + getRootItems(): Promise>; + getChildrenOf(parentUnique: string): Promise>; + getItems(unique: Array): Promise>>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/index.ts index cd4c9e7d46..f4727c8ffc 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/index.ts @@ -1,5 +1,3 @@ -export * from './data-source-response.interface'; +export * from './data-source'; export * from './detail-repository.interface'; export * from './tree-repository.interface'; -export * from './repository-tree-data-source.interface'; -export * from './repository-detail-data-source.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts deleted file mode 100644 index ff4cdd3a6a..0000000000 --- a/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -export interface RepositoryDetailDataSource { - createScaffold(parentKey: string | null): Promise>; - get(key: string): Promise>; - insert(data: DetailType): Promise>; - update(data: DetailType): Promise>; - delete(key: string): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/repository-tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/repository-tree-data-source.interface.ts deleted file mode 100644 index c131d41fff..0000000000 --- a/src/Umbraco.Web.UI.Client/libs/repository/repository-tree-data-source.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EntityTreeItemResponseModel, PagedEntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -export interface RepositoryTreeDataSource { - getRootItems(): Promise>; - getChildrenOf(parentKey: string): Promise>; - getItems(key: Array): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts index a1de9f88ba..4ef19c3920 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts @@ -1,24 +1,29 @@ import type { Observable } from 'rxjs'; -import { EntityTreeItemResponseModel, PagedEntityTreeItemResponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; +import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; -export interface UmbTreeRepository { - requestRootTreeItems: () => Promise<{ - data: PagedEntityTreeItemResponseModel | undefined; - error: ProblemDetailsModel | undefined; - asObservable?: () => Observable; - }>; - requestTreeItemsOf: (parentKey: string | null) => Promise<{ - data: PagedEntityTreeItemResponseModel | undefined; - error: ProblemDetailsModel | undefined; - asObservable?: () => Observable; - }>; - requestTreeItems: (keys: string[]) => Promise<{ - data: Array | undefined; - error: ProblemDetailsModel | undefined; - asObservable?: () => Observable; - }>; - - rootTreeItems: () => Promise>; - treeItemsOf: (parentKey: string | null) => Promise>; - treeItems: (keys: string[]) => Promise>; +export interface UmbPagedData { + total: number; + items: Array; +} + +export interface UmbTreeRepository> { + requestRootTreeItems: () => Promise<{ + data: PagedItemType | undefined; + error: ProblemDetailsModel | undefined; + asObservable?: () => Observable; + }>; + requestTreeItemsOf: (parentUnique: string | null) => Promise<{ + data: PagedItemType | undefined; + error: ProblemDetailsModel | undefined; + asObservable?: () => Observable; + }>; + requestTreeItems: (uniques: string[]) => Promise<{ + data: Array | undefined; + error: ProblemDetailsModel | undefined; + asObservable?: () => Observable; + }>; + + rootTreeItems: () => Promise>; + treeItemsOf: (parentUnique: string | null) => Promise>; + treeItems: (uniques: string[]) => Promise>; } diff --git a/src/Umbraco.Web.UI.Client/libs/store/tree-store-base.ts b/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts similarity index 74% rename from src/Umbraco.Web.UI.Client/libs/store/tree-store-base.ts rename to src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts index 7b2f35893e..1e82f6acaa 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/tree-store-base.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts @@ -1,21 +1,20 @@ import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; /** * @export - * @class UmbTreeStoreBase + * @class UmbEntityTreeStore * @extends {UmbStoreBase} * @description - General Tree Data Store */ -// TODO: consider if tree store could be turned into a general EntityTreeStore class? -export class UmbTreeStoreBase extends UmbStoreBase { +export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore { #data = new ArrayState([], (x) => x.key); /** * Appends items to the store - * @param {Array} items - * @memberof UmbTreeStoreBase + * @param {Array} items + * @memberof UmbEntityTreeStore */ appendItems(items: Array) { this.#data.append(items); @@ -24,8 +23,8 @@ export class UmbTreeStoreBase extends UmbStoreBase { /** * Updates an item in the store * @param {string} key - * @param {Partial} data - * @memberof UmbTreeStoreBase + * @param {Partial} data + * @memberof UmbEntityTreeStore */ updateItem(key: string, data: Partial) { this.#data.next(partialUpdateFrozenArray(this.#data.getValue(), data, (entry) => entry.key === key)); @@ -34,7 +33,7 @@ export class UmbTreeStoreBase extends UmbStoreBase { /** * Removes an item from the store * @param {string} key - * @memberof UmbTreeStoreBase + * @memberof UmbEntityTreeStore */ removeItem(key: string) { this.#data.removeOne(key); @@ -42,7 +41,7 @@ export class UmbTreeStoreBase extends UmbStoreBase { /** * An observable to observe the root items - * @memberof UmbTreeStoreBase + * @memberof UmbEntityTreeStore */ rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); @@ -50,7 +49,7 @@ export class UmbTreeStoreBase extends UmbStoreBase { * Returns an observable to observe the children of a given parent * @param {(string | null)} parentKey * @return {*} - * @memberof UmbTreeStoreBase + * @memberof UmbEntityTreeStore */ childrenOf(parentKey: string | null) { return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); @@ -60,7 +59,7 @@ export class UmbTreeStoreBase extends UmbStoreBase { * Returns an observable to observe the items with the given keys * @param {Array} keys * @return {*} - * @memberof UmbTreeStoreBase + * @memberof UmbEntityTreeStore */ items(keys: Array) { return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); diff --git a/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts b/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts new file mode 100644 index 0000000000..384ff4d883 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts @@ -0,0 +1,67 @@ +import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; + +/** + * @export + * @class UmbFileSystemTreeStore + * @extends {UmbStoreBase} + * @description - General Tree Data Store + */ +export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore { + #data = new ArrayState([], (x) => x.path); + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbFileSystemTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} path + * @param {Partial} data + * @memberof UmbFileSystemTreeStore + */ + updateItem(path: string, data: Partial) { + this.#data.appendOne(data) + } + + /** + * Removes an item from the store + * @param {string} path + * @memberof UmbFileSystemTreeStore + */ + removeItem(path: string) { + this.#data.removeOne(path); + } + + /** + * An observable to observe the root items + * @memberof UmbFileSystemTreeStore + */ + rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false)); + + /** + * Returns an observable to observe the children of a given parent + * @param {(string | null)} parentPath + * @return {*} + * @memberof UmbFileSystemTreeStore + */ + childrenOf(parentPath: string | null) { + return this.#data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/'))); + } + + /** + * Returns an observable to observe the items with the given keys + * @param {Array} paths + * @return {*} + * @memberof UmbFileSystemTreeStore + */ + items(paths: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? ''))); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/store/index.ts b/src/Umbraco.Web.UI.Client/libs/store/index.ts index 865fdf2b5a..f9152d3ebd 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/index.ts @@ -1,3 +1,5 @@ export * from './store'; export * from './store-base'; -export * from './tree-store-base'; +export * from './entity-tree-store'; +export * from './file-system-tree.store'; +export * from './tree-store.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/store/store.ts b/src/Umbraco.Web.UI.Client/libs/store/store.ts index 083cc9fc2f..d4af9c2275 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/store.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/store.ts @@ -9,33 +9,7 @@ export interface UmbDataStore { readonly storeAlias: string; } -export interface UmbTreeStore extends UmbDataStore { - - getTreeRoot(): Observable>; - - getTreeItemChildren(key: string): Observable>; - - // Notice: this might not be right to put here as only some content items has ability to be trashed. - /** - * @description - Trash data. - * @param {string[]} keys - * @return {*} {(Promise)} - * @memberof UmbTreeStore - */ - trash(keys: string[]): Promise; - - // Notice: this might not be right to put here as only some content items has ability to be moved. - /** - * @description - Move data. - * @param {string[]} keys - * @return {*} {(Promise)} - * @memberof UmbTreeStore - */ - move(keys: string[], destination: string): Promise; -} - export interface UmbEntityDetailStore extends UmbDataStore { - /** * @description - Request scaffold data by entityType and . The data is added to the store and is returned as an Observable. * @param {string} key @@ -61,10 +35,7 @@ export interface UmbEntityDetailStore extends UmbDataStore { save(data: T[]): Promise; } - export interface UmbContentStore extends UmbEntityDetailStore { - // TODO: make something that is specific for UmbContentStore, or then we should get rid of it. But for now i kept it as we might want this for rollback or other things specific to Content types. save(data: T[]): Promise; - } diff --git a/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts b/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts new file mode 100644 index 0000000000..80a366ada5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts @@ -0,0 +1,12 @@ +import type { Observable } from 'rxjs'; +import { TreeItemPresentationModel } from '../backend-api'; + +export interface UmbTreeStore { + appendItems: (items: Array) => void; + updateItem: (unique: string, item: Partial) => void; + removeItem: (unique: string) => void; + + rootItems: Observable>; + childrenOf: (parentUnique: string | null) => Observable>; + items: (uniques: Array) => Observable>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts b/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts index 38dd1744eb..dde528e171 100644 --- a/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts @@ -1,4 +1,4 @@ -import { UmbWorkspaceContextInterface } from '../../../../src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface'; +import { UmbWorkspaceContextInterface } from '../../context/workspace-context.interface'; import { UmbWorkspaceActionBase } from '../workspace-action-base'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; diff --git a/src/Umbraco.Web.UI.Client/libs/workspace/context/index.ts b/src/Umbraco.Web.UI.Client/libs/workspace/context/index.ts new file mode 100644 index 0000000000..c4aea806a5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/workspace/context/index.ts @@ -0,0 +1 @@ +export * from './workspace-context.interface'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/libs/workspace/context/workspace-context.interface.ts similarity index 80% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts rename to src/Umbraco.Web.UI.Client/libs/workspace/context/workspace-context.interface.ts index c518155e53..d5ef7d313b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/workspace/context/workspace-context.interface.ts @@ -7,6 +7,7 @@ export interface UmbWorkspaceContextInterface { isNew: Observable; getIsNew(): boolean; setIsNew(value: boolean): void; + // TODO: should we consider another name than entity type. File system files are not entities but still have this type. getEntityType(): string; getData(): T; destroy(): void; diff --git a/src/Umbraco.Web.UI.Client/libs/workspace/index.ts b/src/Umbraco.Web.UI.Client/libs/workspace/index.ts index 485f1b10af..de14fcfee4 100644 --- a/src/Umbraco.Web.UI.Client/libs/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/workspace/index.ts @@ -1 +1,2 @@ export * from './actions'; +export * from './context'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts index fb1968d7a2..cbb0c01bbb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; export const UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( @@ -12,7 +12,7 @@ export const UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN = new UmbContextTok * @extends {UmbStoreBase} * @description - Tree Data Store for Document Blueprints */ -export class UmbDocumentBlueprintTreeStore extends UmbTreeStoreBase { +export class UmbDocumentBlueprintTreeStore extends UmbEntityTreeStore { constructor(host: UmbControllerHostInterface) { super(host, UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN.toString()); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts index 7191342a68..aae1b52131 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts @@ -2,11 +2,7 @@ import { DocumentTypeTreeServerDataSource } from './sources/document-type.tree.s import { UmbDocumentTypeServerDataSource } from './sources/document-type.server.data'; import { UmbDocumentTypeTreeStore, UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN } from './document-type.tree.store'; import { UmbDocumentTypeStore, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN } from './document-type.store'; -import type { - RepositoryTreeDataSource, - UmbTreeRepository, - UmbDetailRepository, -} from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -14,12 +10,12 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco type ItemType = DocumentTypeResponseModel; -export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRepository { #init!: Promise; #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbDocumentTypeTreeStore; #detailDataSource: UmbDocumentTypeServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.tree.store.ts index 7cfdee24df..4052a3ff76 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.tree.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** @@ -9,7 +9,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; * @description - Tree Data Store for Document-Types */ // TODO: consider if tree store could be turned into a general EntityTreeStore class? -export class UmbDocumentTypeTreeStore extends UmbTreeStoreBase { +export class UmbDocumentTypeTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbDocumentTypeTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts index 358910aabc..f5819640ff 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts @@ -1,5 +1,9 @@ -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; -import { DocumentTypeResource, ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; +import { + DocumentTypeResource, + ProblemDetailsModel, + DocumentTypeResponseModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -9,7 +13,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDocumentTypeServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentTypeServerDataSource implements RepositoryDetailDataSource { +export class UmbDocumentTypeServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.tree.server.data.ts index 1d7deb1778..6d5f8877f4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.tree.server.data.ts @@ -1,4 +1,4 @@ -import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, DocumentTypeResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class DocumentTreeServerDataSource * @implements {DocumentTreeDataSource} */ -export class DocumentTypeTreeServerDataSource implements RepositoryTreeDataSource { +export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; // TODO: how do we handle trashed items? diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts index fbe7421b2c..5a5ccaa08d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts @@ -1,13 +1,23 @@ -import { UmbDocumentTypeRepository } from '../repository/document-type.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { DOCUMENT_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.DocumentTypes', name: 'Document Types Tree', meta: { - repository: UmbDocumentTypeRepository, + repositoryAlias: DOCUMENT_TYPE_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.DocumentType', + name: 'Document Type Tree Item', + conditions: { + entityType: 'document-type', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-edit.element.ts index 205a57e93d..cf68341d44 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-edit.element.ts @@ -105,7 +105,18 @@ export class UmbDocumentTypeWorkspaceEditElement extends UmbLitElement { -
Keyboard Shortcuts
+
+ + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
`; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts index b596be3178..8854d3a53a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts @@ -25,7 +25,52 @@ const workspaceViews: Array = [ meta: { label: 'Design', pathname: 'design', - icon: 'edit', + icon: 'umb:document-dashed-line', + }, + conditions: { + workspaces: ['Umb.Workspace.DocumentType'], + }, + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.DocumentType.ListView', + name: 'Document Type Workspace List View', + loader: () => import('./views/listview/workspace-view-document-type-listview.element'), + weight: 100, + meta: { + label: 'Listview', + pathname: 'listview', + icon: 'umb:list', + }, + conditions: { + workspaces: ['Umb.Workspace.DocumentType'], + }, + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.DocumentType.Permissions', + name: 'Document Type Workspace Permissions View', + loader: () => import('./views/permissions/workspace-view-document-type-permissions.element'), + weight: 100, + meta: { + label: 'Permissions', + pathname: 'permissions', + icon: 'umb:keychain', + }, + conditions: { + workspaces: ['Umb.Workspace.DocumentType'], + }, + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.DocumentType.Templates', + name: 'Document Type Workspace Templates View', + loader: () => import('./views/templates/workspace-view-document-type-templates.element'), + weight: 100, + meta: { + label: 'Templates', + pathname: 'templates', + icon: 'umb:layout', }, conditions: { workspaces: ['Umb.Workspace.DocumentType'], diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts index d4007d6ccc..0d8f2b1e15 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts @@ -1,6 +1,7 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, state } from 'lit/decorators.js'; +import { customElement, query, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -13,8 +14,66 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { css` :host { display: block; + } + /* TODO: This should be replaced with a general workspace bar — naming is hard */ + #workspace-tab-bar { + padding: 0 var(--uui-size-layout-1); + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--uui-color-surface); + flex-wrap: nowrap; + } + .tab-actions { + display: flex; + gap: var(--uui-size-space-4); + } + .tab-actions uui-button uui-icon { + padding-right: calc(-1 * var(--uui-size-space-4)); + } + + uui-tab { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + } + + uui-tab .trash { + display: flex; + align-items: stretch; + } + + uui-tab uui-input { + flex-grow: 1; + } + + uui-input:not(:focus) { + border: 1px solid transparent; + } + + uui-input:not(:hover, :focus) .trash { + opacity: 0; + } + + /** Property Group Wrapper */ + + #wrapper { margin: var(--uui-size-layout-1); } + + #add-group { + margin-top: var(--uui-size-layout-1); + width: 100%; + --uui-button-height: var(--uui-size-layout-4); + } + + .group-headline { + display: flex; + gap: var(--uui-size-space-4); + } + .group-headline uui-input { + flex-grow: 1; + } `, ]; @@ -23,6 +82,9 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + @state() + private _tabs: any[] = []; + constructor() { super(); @@ -42,12 +104,70 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { } render() { - return html` Design of ${this._documentType?.name} - -
- + return html` +
+ ${this.renderTabBar()} +
+ + + Compositions + + + + Recorder +
- `; +
+
+ +
+ + + +
+ +
+ Add group +
+ `; + } + + #remove(index: number) { + this._tabs.splice(index, 1); + this.requestUpdate(); + } + async #addTab() { + this._tabs = [...this._tabs, { name: 'Test' }]; + } + + renderTabBar() { + return html` + ${repeat( + this._tabs, + (tab) => tab.name, + (tab, index) => { + //TODO: Should these tabs be part of routing? + return html` +
+ + + + + +
+
`; + } + )} + + + Add tab + +
`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/listview/workspace-view-document-type-listview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/listview/workspace-view-document-type-listview.element.ts new file mode 100644 index 0000000000..b6c89a8d1d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/listview/workspace-view-document-type-listview.element.ts @@ -0,0 +1,46 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +@customElement('umb-workspace-view-document-type-listview') +export class UmbWorkspaceViewDocumentTypeListviewElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + @state() + _documentType?: DocumentTypeResponseModel; + + private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + + constructor() { + super(); + + // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken + this.consumeContext('umbWorkspaceContext', (documentTypeContext) => { + this._workspaceContext = documentTypeContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this._workspaceContext) return; + + this.observe(this._workspaceContext.data, (documentType) => { + this._documentType = documentType; + }); + } + + render() { + return html` Listview `; + } +} + +export default UmbWorkspaceViewDocumentTypeListviewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-view-document-type-listview': UmbWorkspaceViewDocumentTypeListviewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/permissions/workspace-view-document-type-permissions.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/permissions/workspace-view-document-type-permissions.element.ts new file mode 100644 index 0000000000..27284e7c24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/permissions/workspace-view-document-type-permissions.element.ts @@ -0,0 +1,100 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-workspace-view-document-type-permissions') +export class UmbWorkspaceViewDocumentTypePermissionsElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + margin: var(--uui-size-layout-1); + } + uui-label, + umb-property-editor-ui-number { + display: block; + } + + uui-toggle { + display: flex; + } + `, + ]; + + @state() + _documentType?: DocumentTypeResponseModel; + + private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + + constructor() { + super(); + + // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken + this.consumeContext('umbWorkspaceContext', (documentTypeContext) => { + this._workspaceContext = documentTypeContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this._workspaceContext) return; + + this.observe(this._workspaceContext.data, (documentType) => { + this._documentType = documentType; + }); + } + + render() { + return html` + + +
Allow editors to create content of this type in the root of the content tree.
+
+
+ +
+ Allow content of the specified types to be created underneath content of this type. +
+
+ +
+
+ +
Allow editors to create content of different languages.
+
+
+ +
+ An Element Type is meant to be used for instance in Nested Content, and not in the tree.
+ A Document Type cannot be changed to an Element Type once it has been used to create one or more content + items. +
+
+
+ +
Allow overriding the global history cleanup settings.
+
+ + Keep all versions newer than days + + Keep latest version per day for days + +
+
+
+ `; + } +} + +export default UmbWorkspaceViewDocumentTypePermissionsElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-view-document-type-permissions': UmbWorkspaceViewDocumentTypePermissionsElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/workspace-view-document-type-templates.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/workspace-view-document-type-templates.element.ts new file mode 100644 index 0000000000..5b53aef5e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/workspace-view-document-type-templates.element.ts @@ -0,0 +1,91 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-workspace-view-document-type-templates') +export class UmbWorkspaceViewDocumentTypeTemplatesElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + margin: var(--uui-size-layout-1); + } + + #templates { + text-align: center; + } + + #template-card-wrapper { + display: flex; + gap: var(--uui-size-space-4); + align-items: stretch; + } + + umb-workspace-property-layout { + border-top: 1px solid var(--uui-color-border); + } + umb-workspace-property-layout:first-child { + padding-top: 0; + border: none; + } + `, + ]; + + @state() + _documentType?: DocumentTypeResponseModel; + + private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + + constructor() { + super(); + this.consumeContext('umbWorkspaceContext', (documentTypeContext) => { + this._workspaceContext = documentTypeContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this._workspaceContext) return; + + this.observe(this._workspaceContext.data, (documentType) => { + this._documentType = documentType; + }); + } + + async #changeDefaultKey(e: CustomEvent) { + // save new default key + console.log('workspace: default template key', e); + } + + #changeAllowedKeys(e: CustomEvent) { + // save new allowed keys + console.log('workspace: allowed templates changed', e); + } + + render() { + return html` + +
Choose which templates editors are allowed to use on content of this type
+
+ +
+
+
`; + } +} + +export default UmbWorkspaceViewDocumentTypeTemplatesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-view-document-type-templates': UmbWorkspaceViewDocumentTypeTemplatesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts index db1abd9914..61718098ac 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts @@ -1,5 +1,5 @@ import { manifests as collectionManifests } from './collection/manifests'; -import { manifests as menuItemManifests } from './sidebar-menu-item/manifests'; +import { manifests as menuItemManifests } from './menu-item/manifests'; import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/document-sidebar-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/document-menu-item.element.ts similarity index 54% rename from src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/document-sidebar-menu-item.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/document-menu-item.element.ts index 7102a30105..07d94f6a1f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/document-sidebar-menu-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/document-menu-item.element.ts @@ -2,17 +2,17 @@ import { html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -@customElement('umb-document-sidebar-menu-item') -export class UmbDocumentSidebarMenuItemElement extends UmbLitElement { +@customElement('umb-document-menu-item') +export class UmbDocumentMenuItemElement extends UmbLitElement { render() { return html``; } } -export default UmbDocumentSidebarMenuItemElement; +export default UmbDocumentMenuItemElement; declare global { interface HTMLElementTagNameMap { - 'umb-document-sidebar-menu-item': UmbDocumentSidebarMenuItemElement; + 'umb-document-menu-item': UmbDocumentMenuItemElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/manifests.ts similarity index 85% rename from src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/manifests.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/manifests.ts index e06fc87a74..5c448b6d0b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/sidebar-menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/menu-item/manifests.ts @@ -5,7 +5,7 @@ const menuItem: ManifestMenuItem = { alias: 'Umb.MenuItem.Documents', name: 'Documents Menu Item', weight: 100, - loader: () => import('./document-sidebar-menu-item.element'), + loader: () => import('./document-menu-item.element'), meta: { label: 'Documents', icon: 'umb:folder', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts new file mode 100644 index 0000000000..0e26fd7588 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts @@ -0,0 +1,103 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import type { UmbTreeElement } from '../../../../shared/components/tree/tree.element'; +import { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; + +// TODO: make use of UmbPickerLayoutBase +@customElement('umb-document-type-picker-modal') +export class UmbDocumentTypePickerModalElement extends UmbModalBaseElement< + UmbDocumentTypePickerModalData, + UmbDocumentTypePickerModalResult +> { + static styles = [ + UUITextStyles, + css` + h3 { + margin-left: var(--uui-size-space-5); + margin-right: var(--uui-size-space-5); + } + + uui-input { + width: 100%; + } + + hr { + border: none; + border-bottom: 1px solid var(--uui-color-divider); + margin: 16px 0; + } + + #content-list { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + } + + .content-item { + cursor: pointer; + } + + .content-item.selected { + background-color: var(--uui-color-selected); + color: var(--uui-color-selected-contrast); + } + `, + ]; + + @state() + _selection: Array = []; + + @state() + _multiple = true; + + connectedCallback() { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this._multiple = this.data?.multiple ?? true; + } + + private _handleSelectionChange(e: CustomEvent) { + e.stopPropagation(); + const element = e.target as UmbTreeElement; + //TODO: Should multiple property be implemented here or be passed down into umb-tree? + this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; + } + + private _submit() { + this.modalHandler?.submit({ selection: this._selection }); + } + + private _close() { + this.modalHandler?.reject(); + } + + render() { + return html` + + + +
+ +
+
+ + +
+
+ `; + } +} + +export default UmbDocumentTypePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-type-picker-modal': UmbDocumentTypePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts new file mode 100644 index 0000000000..e51a73bc24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts @@ -0,0 +1,26 @@ +import '../../../../shared/components/body-layout/body-layout.element'; +import './document-type-picker-modal.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit'; + +import type { UmbDocumentTypePickerModalElement } from './document-type-picker-modal.element'; +import type { UmbDocumentTypePickerModalData } from './index'; + +export default { + title: 'API/Modals/Layouts/Content Picker', + component: 'umb-document-type-picker-modal', + id: 'umb-document-type-picker-modal', +} as Meta; + +const data: UmbDocumentTypePickerModalData = { + multiple: true, + selection: [], +}; + +export const Overview: Story = () => html` + + +`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/index.ts new file mode 100644 index 0000000000..8c7834fd28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDocumentTypePickerModalData { + multiple?: boolean; + selection?: Array; +} + +export interface UmbDocumentTypePickerModalResult { + selection: Array; +} + +export const UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN = new UmbModalToken< + UmbDocumentTypePickerModalData, + UmbDocumentTypePickerModalResult +>('Umb.Modal.DocumentTypePicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts index ae03c13d31..5e9595e6a0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts @@ -7,6 +7,12 @@ const modals: Array = [ name: 'Document Picker Modal', loader: () => import('./document-picker/document-picker-modal.element'), }, + { + type: 'modal', + alias: 'Umb.Modal.DocumentTypePicker', + name: 'Document Type Picker Modal', + loader: () => import('./document-type-picker/document-type-picker-modal.element'), + }, ]; export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index 8bc17134bd..5e80ce51f9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -1,12 +1,8 @@ -import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; -import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; -import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store'; import { UmbDocumentServerDataSource } from './sources/document.server.data'; -import type { - RepositoryTreeDataSource, - UmbTreeRepository, - UmbDetailRepository, -} from '@umbraco-cms/backoffice/repository'; +import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store'; +import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; +import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; +import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { ProblemDetailsModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -14,16 +10,12 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco type ItemType = DocumentResponseModel; -// 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 UmbDocumentRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailRepository { #init!: Promise; #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbDocumentTreeStore; #detailDataSource: UmbDocumentServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.tree.store.ts index 4b3bbaa7f8..4cab5a9ac3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.tree.store.ts @@ -1,14 +1,14 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbDocumentTreeStore - * @extends {UmbTreeStoreBase} + * @extends {UmbEntityTreeStore} * @description - Tree Data Store for Templates */ -export class UmbDocumentTreeStore extends UmbTreeStoreBase { +export class UmbDocumentTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbDocumentTreeStore. * @param {UmbControllerHostInterface} host @@ -19,6 +19,4 @@ export class UmbDocumentTreeStore extends UmbTreeStoreBase { } } -export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbDocumentTreeStore' -); +export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentTreeStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts index 467b0fa35f..c68b6a8eb1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; import { DocumentResource, ProblemDetailsModel, @@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDocumentServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentServerDataSource implements RepositoryDetailDataSource { +export class UmbDocumentServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts index 17ab86a2fb..3dd6b3d6ff 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts @@ -1,4 +1,4 @@ -import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, DocumentResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class DocumentTreeServerDataSource * @implements {DocumentTreeDataSource} */ -export class DocumentTreeServerDataSource implements RepositoryTreeDataSource { +export class DocumentTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; // TODO: how do we handle trashed items? diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/index.ts index ac6a6497ed..de97e4eed6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/index.ts @@ -1,7 +1,7 @@ import type { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { RepositoryDetailDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository'; -export interface UmbDocumentDataSource extends RepositoryDetailDataSource { +export interface UmbDocumentDataSource extends UmbDataSource { createScaffold(documentTypeKey: string): Promise>; trash(key: string): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts index c1e6a79314..a5dbe90321 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts @@ -1,5 +1,5 @@ -import { UmbDocumentRepository } from '../repository/document.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const treeAlias = 'Umb.Tree.Documents'; @@ -8,8 +8,18 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Documents Tree', meta: { - repository: UmbDocumentRepository, // TODO: use alias instead of class + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + alias: 'Umb.TreeItem.Document', + name: 'Document Tree Item', + loader: () => import('./tree-item/document-tree-item.element'), + conditions: { + entityType: 'document', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.context.ts new file mode 100644 index 0000000000..19cfaf1ccd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.context.ts @@ -0,0 +1,10 @@ +import { UmbTreeItemContextBase } from '../../../../shared/components/tree/tree-item-base/tree-item-base.context'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO get unique method from an document repository static method +export class UmbDocumentTreeItemContext extends UmbTreeItemContextBase { + constructor(host: UmbControllerHostInterface) { + super(host, (x: DocumentTreeItemResponseModel) => x.key); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.element.ts new file mode 100644 index 0000000000..e71f162a4b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/tree-item/document-tree-item.element.ts @@ -0,0 +1,78 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbDocumentTreeItemContext } from './document-tree-item.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-document-tree-item') +export class UmbDocumentTreeItemElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #icon-container { + position: relative; + } + + #icon { + vertical-align: middle; + } + + #status-symbol { + width: 8px; + height: 8px; + background-color: blue; + display: block; + position: absolute; + bottom: 0; + right: 0; + border-radius: 100%; + } + `, + ]; + + private _item?: DocumentTreeItemResponseModel; + @property({ type: Object, attribute: false }) + public get item() { + return this._item; + } + public set item(value: DocumentTreeItemResponseModel | undefined) { + this._item = value; + this.#context.setTreeItem(value); + } + + #context = new UmbDocumentTreeItemContext(this); + + render() { + if (!this.item) return nothing; + return html` + ${this.#renderIconWithStatusSymbol()} ${this.#renderLabel()} + `; + } + + // TODO: implement correct status symbol + #renderIconWithStatusSymbol() { + return html` + + ${this.item?.icon + ? html` + + ` + : nothing} + + `; + } + + // TODO: lower opacity if item is not published + #renderLabel() { + return html` ${this.item?.name} `; + } +} + +export default UmbDocumentTreeItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-tree-item': UmbDocumentTreeItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts index 5bdfda591d..011e67a7e1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts @@ -7,14 +7,14 @@ import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-ap import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; -import { UmbTreeRepository, RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeRepository, UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; export class UmbMediaTypeRepository implements UmbTreeRepository { #init!: Promise; #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbMediaTypeTreeStore; #detailSource: UmbMediaTypeDetailServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts index 6e6f69ed7d..3392acb8d1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts @@ -1,14 +1,14 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbMediaTypeTreeStore - * @extends {UmbTreeStoreBase} + * @extends {UmbEntityTreeStore} * @description - Tree Data Store for Media Types */ -export class UmbMediaTypeTreeStore extends UmbTreeStoreBase { +export class UmbMediaTypeTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbMediaTypeTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts index b246beefef..3c2f57bf07 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts @@ -1,6 +1,6 @@ import { MediaTypeResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class MediaTypeTreeServerDataSource * @implements {MediaTypeTreeDataSource} */ -export class MediaTypeTreeServerDataSource implements RepositoryTreeDataSource { +export class MediaTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts index 526aff49b2..fff9c37137 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts @@ -1,13 +1,23 @@ -import { UmbMediaTypeRepository } from '../repository/media-type.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { MEDIA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.MediaTypes', name: 'Media Types Tree', meta: { - repository: UmbMediaTypeRepository, + repositoryAlias: MEDIA_TYPE_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.MediaType', + name: 'Media Type Tree Item', + conditions: { + entityType: 'media-type', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts index 97f159183e..ab2d992be6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts @@ -3,21 +3,20 @@ import { MediaTreeServerDataSource } from './sources/media.tree.server.data'; import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store'; import { UmbMediaStore, UMB_MEDIA_STORE_CONTEXT_TOKEN } from './media.store'; import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data'; -import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; -import { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; +import type { UmbTreeRepository } from 'libs/repository/tree-repository.interface'; type ItemDetailType = MediaDetails; export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepository { - #init!: Promise; - #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbMediaTreeStore; #detailDataSource: UmbMediaDetailServerDataSource; @@ -25,6 +24,9 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor #notificationContext?: UmbNotificationContext; + #initResolver?: () => void; + #initialized = false; + constructor(host: UmbControllerHostInterface) { this.#host = host; @@ -32,19 +34,32 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor this.#treeSource = new MediaTreeServerDataSource(this.#host); this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host); - this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this.#treeStore = instance; - }), + new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); - new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => { - this.#store = instance; - }), + new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; + this.#checkIfInitialized(); + }); - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - }), - ]); + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationContext = instance; + this.#checkIfInitialized(); + }); + } + + // TODO: make a generic way to wait for initialization + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + + #checkIfInitialized() { + if (this.#treeStore && this.#store && this.#notificationContext) { + this.#initialized = true; + this.#initResolver?.(); + } } async requestRootTreeItems() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts index 88903b1cb1..4214a9d7f2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts @@ -1,7 +1,7 @@ import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTreeStore'); @@ -9,10 +9,10 @@ export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.key); /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts index 57b1fde3c7..120cb495e8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts @@ -1,5 +1,5 @@ import type { MediaDetails } from '../../'; -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -10,7 +10,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbTemplateDetailServerDataSource * @implements {TemplateDetailDataSource} */ -export class UmbMediaDetailServerDataSource implements RepositoryDetailDataSource { +export class UmbMediaDetailServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts index b2b2cd2603..75c88bf7b4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts @@ -1,4 +1,4 @@ -import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, MediaResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class MediaTreeServerDataSource * @implements {MediaTreeDataSource} */ -export class MediaTreeServerDataSource implements RepositoryTreeDataSource { +export class MediaTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; // TODO: how do we handle trashed items? diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts index 6f40b6d6ee..b66042c3ba 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts @@ -1,5 +1,5 @@ -import { UmbMediaRepository } from '../repository/media.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { MEDIA_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const treeAlias = 'Umb.Tree.Media'; @@ -8,8 +8,18 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Media Tree', meta: { - repository: UmbMediaRepository, // TODO: use alias instead of class + repositoryAlias: MEDIA_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.Media', + name: 'Media Tree Item', + conditions: { + entityType: 'media', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts index 0ed1749225..5ca8bcbac1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts @@ -7,11 +7,7 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; -import type { - RepositoryTreeDataSource, - UmbDetailRepository, - UmbTreeRepository, -} from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; // TODO => Update type when backend updated export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRepository { @@ -19,7 +15,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbMemberGroupTreeStore; #detailSource: UmbMemberGroupDetailServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts index 1a67085088..6e20bb0447 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts @@ -1,14 +1,14 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbMemberGroupTreeStore - * @extends {UmbTreeStoreBase} + * @extends {UmbEntityTreeStore} * @description - Tree Data Store for Member Groups */ -export class UmbMemberGroupTreeStore extends UmbTreeStoreBase { +export class UmbMemberGroupTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbMemberGroupTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.detail.server.data.ts index 91d1b54cd7..544de51bc6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.detail.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.detail.server.data.ts @@ -2,7 +2,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models'; -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; /** * @description - A data source for the MemberGroup detail that fetches data from the server @@ -11,7 +11,7 @@ import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; * @implements {MemberGroupDetailDataSource} */ // TODO => Provide type when it is available -export class UmbMemberGroupDetailServerDataSource implements RepositoryDetailDataSource { +export class UmbMemberGroupDetailServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; constructor(host: UmbControllerHostInterface) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts index 6ccabbbf3c..5388958abd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts @@ -1,6 +1,6 @@ import { MemberGroupResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class MemberGroupTreeServerDataSource * @implements {MemberGroupTreeDataSource} */ -export class MemberGroupTreeServerDataSource implements RepositoryTreeDataSource { +export class MemberGroupTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; /** @@ -22,42 +22,42 @@ export class MemberGroupTreeServerDataSource implements RepositoryTreeDataSource } /** - * Fetches the root items for the tree from the server - * @return {*} - * @memberof MemberGroupTreeServerDataSource - */ - async getRootItems() { - return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({})); - } + * Fetches the root items for the tree from the server + * @return {*} + * @memberof MemberGroupTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({})); + } - /** - * Fetches the children of a given parent key from the server - * @param {(string | null)} parentKey - * @return {*} - * @memberof MemberGroupTreeServerDataSource - */ - async getChildrenOf(parentKey: string | null) { - // Not implemented for this tree - return {}; - } + /** + * Fetches the children of a given parent key from the server + * @param {(string | null)} parentKey + * @return {*} + * @memberof MemberGroupTreeServerDataSource + */ + async getChildrenOf(parentKey: string | null) { + // Not implemented for this tree + return {}; + } - /** - * Fetches the items for the given keys from the server - * @param {Array} keys - * @return {*} - * @memberof MemberGroupTreeServerDataSource - */ - async getItems(keys: Array) { - if (!keys || keys.length === 0) { - const error: ProblemDetailsModel = { title: 'Keys are missing' }; - return { error }; - } + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof MemberGroupTreeServerDataSource + */ + async getItems(keys: Array) { + if (!keys || keys.length === 0) { + const error: ProblemDetailsModel = { title: 'Keys are missing' }; + return { error }; + } - return tryExecuteAndNotify( - this.#host, - MemberGroupResource.getTreeMemberGroupItem({ - key: keys, - }) - ); - } + return tryExecuteAndNotify( + this.#host, + MemberGroupResource.getTreeMemberGroupItem({ + key: keys, + }) + ); + } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts index ee8ef370fb..5d379a3782 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts @@ -1,5 +1,5 @@ -import { UmbMemberGroupRepository } from '../repository/member-group.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { MEMBER_GROUP_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const treeAlias = 'Umb.Tree.MemberGroups'; @@ -9,8 +9,18 @@ const tree: ManifestTree = { name: 'Member Groups Tree', weight: 100, meta: { - repository: UmbMemberGroupRepository, + repositoryAlias: MEMBER_GROUP_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.MemberGroup', + name: 'Member Group Tree Item', + conditions: { + entityType: 'member-group', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts index 5b7f0cf65d..ffa47cbd04 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts @@ -4,20 +4,21 @@ import { UmbMemberTypeStore, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN } from './membe import { UmbMemberTypeDetailServerDataSource } from './sources/member-type.detail.server.data'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { RepositoryTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import type { MemberTypeDetails } from '@umbraco-cms/backoffice/models'; // TODO => use correct type when available type ItemType = any; +type TreeItemType = any; -export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepository { #init!: Promise; #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbMemberTypeTreeStore; #detailSource: UmbMemberTypeDetailServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts index f008de15aa..8fb32d6bea 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** @@ -8,7 +8,7 @@ import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/control * @extends {UmbStoreBase} * @description - Tree Data Store for Member Types */ -export class UmbMemberTypeTreeStore extends UmbTreeStoreBase { +export class UmbMemberTypeTreeStore extends UmbEntityTreeStore { constructor(host: UmbControllerHostInterface) { super(host, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts index 1384165787..4f450b5bde 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts @@ -1,6 +1,6 @@ import { MemberTypeResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class MemberTypeTreeServerDataSource * @implements {MemberTypeTreeDataSource} */ -export class MemberTypeTreeServerDataSource implements RepositoryTreeDataSource { +export class MemberTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts index 042a06d314..6781a419f4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts @@ -1,5 +1,5 @@ -import { UmbMemberTypeRepository } from '../repository/member-type.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { MEMBER_TYPES_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const treeAlias = 'Umb.Tree.MemberTypes'; @@ -8,8 +8,18 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Member Types Tree', meta: { - repository: UmbMemberTypeRepository, + repositoryAlias: MEMBER_TYPES_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.MemberType', + name: 'Member Type Tree Item', + conditions: { + entityType: 'member-type', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts index b315e84c5a..a5c832358e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTreeStore'); @@ -7,10 +7,10 @@ export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken 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 UmbDataTypeRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbDataTypeRepository implements UmbTreeRepository, UmbDetailRepository { #init!: Promise; #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbDataTypeTreeStore; #detailDataSource: UmbDataTypeServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.tree.store.ts index 9e079f39bd..455bf769f5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.tree.store.ts @@ -1,6 +1,6 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; /** * @export @@ -9,7 +9,7 @@ import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; * @description - Tree Data Store for Data-Types */ // TODO: consider if tree store could be turned into a general EntityTreeStore class? -export class UmbDataTypeTreeStore extends UmbTreeStoreBase { +export class UmbDataTypeTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbDataTypeTreeStore. * @param {UmbControllerHostInterface} host @@ -20,6 +20,4 @@ export class UmbDataTypeTreeStore extends UmbTreeStoreBase { } } -export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbDataTypeTreeStore' -); +export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDataTypeTreeStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts index 375237041e..86585ea6f1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, DataTypeResource, @@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDataTypeServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDataTypeServerDataSource implements RepositoryDetailDataSource { +export class UmbDataTypeServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts index ae8ebd987a..933a057a7b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts @@ -1,4 +1,4 @@ -import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class DocumentTreeServerDataSource * @implements {DocumentTreeDataSource} */ -export class DataTypeTreeServerDataSource implements RepositoryTreeDataSource { +export class DataTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; // TODO: how do we handle trashed items? 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 6aef98a542..49c0a602a4 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,13 +1,23 @@ -import { UmbDataTypeRepository } from '../repository/data-type.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { DATA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.DataTypes', name: 'Data Types Tree', meta: { - repository: UmbDataTypeRepository, + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.DataType', + name: 'Data Type Tree Item', + conditions: { + entityType: 'data-type', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts index 3a2bfbb366..4a3e236af4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts @@ -1,5 +1,5 @@ import { LanguageResponseModel, PagedLanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { RepositoryDetailDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository'; // TODO: This is a temporary solution until we have a proper paging interface type paging = { @@ -7,7 +7,7 @@ type paging = { take: number; }; -export interface UmbLanguageDataSource extends RepositoryDetailDataSource { +export interface UmbLanguageDataSource extends UmbDataSource { createScaffold(): Promise>; get(isoCode: string): Promise>; delete(isoCode: string): Promise>; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/manifests.ts index 8138e8f9c9..1e6694da7d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/manifests.ts @@ -1,15 +1,15 @@ -import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extensions-registry'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; -const menuItem: ManifestMenuItem = { +const menuItem: ManifestTypes = { type: 'menuItem', + kind: 'tree', alias: 'Umb.MenuItem.RelationTypes', name: 'Relation Types Menu Item', weight: 40, - loader: () => import('./relation-types-menu-item.element'), meta: { + treeAlias: 'Umb.Tree.RelationTypes', label: 'Relation Types', icon: 'umb:folder', - entityType: 'relation-type', }, conditions: { menus: ['Umb.Menu.Settings'], diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/relation-types-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/relation-types-menu-item.element.ts deleted file mode 100644 index f9973b6dcb..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/menu-item/relation-types-menu-item.element.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { html, nothing } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - -@customElement('umb-relation-types-menu-item') -export class UmbRelationTypesMenuItemElement 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 UmbRelationTypesMenuItemElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-relation-types-menu-item': UmbRelationTypesMenuItemElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts index 4c068d3a48..201aa04df7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts @@ -10,12 +10,13 @@ import { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; type ItemType = RelationTypeResponseModel; +type TreeItemType = any; // 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 UmbRelationTypeRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbRelationTypeRepository implements UmbTreeRepository, UmbDetailRepository { #init!: Promise; #host: UmbControllerHostInterface; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.tree.store.ts index 0220ccaa87..b66af29604 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.tree.store.ts @@ -1,6 +1,6 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; /** * @export @@ -9,7 +9,7 @@ import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; * @description - Tree Data Store for relation-types */ // TODO: consider if tree store could be turned into a general EntityTreeStore class? -export class UmbRelationTypeTreeStore extends UmbTreeStoreBase { +export class UmbRelationTypeTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbRelationTypeTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.server.data.ts index d7756f9ae9..b16a93ebc8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.server.data.ts @@ -1,4 +1,4 @@ -import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel, RelationTypeResource, @@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbRelationTypeServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbRelationTypeServerDataSource implements RepositoryDetailDataSource { +export class UmbRelationTypeServerDataSource implements UmbDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/tree/manifests.ts index 9b806c1dd6..42dc1f26cf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/tree/manifests.ts @@ -1,13 +1,23 @@ -import { UmbRelationTypeRepository } from '../repository/relation-type.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { RELATION_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.RelationTypes', name: 'Relation Types Tree', meta: { - repository: UmbRelationTypeRepository, + repositoryAlias: RELATION_TYPE_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.RelationType', + name: 'Relation Type Tree Item', + conditions: { + entityType: 'relation-type', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts index d520450aa7..61fef23527 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts @@ -1,6 +1,5 @@ import { Observable } from 'rxjs'; import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import type { UmbTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextToken, UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; @@ -8,17 +7,14 @@ import { umbExtensionsRegistry, createExtensionClass } from '@umbraco-cms/backof import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; // TODO: Clean up the need for store as Media has switched to use Repositories(repository). -export class UmbCollectionContext< - DataType extends EntityTreeItemResponseModel = EntityTreeItemResponseModel, - StoreType extends UmbTreeStore = UmbTreeStore -> { +export class UmbCollectionContext { private _host: UmbControllerHostInterface; private _entityType: string | null; private _entityKey: string | null; - #repository?: UmbTreeRepository; + #repository?: UmbTreeRepository; - private _store?: StoreType; + private _store?: any; protected _dataObserver?: UmbObserverController; #data = new ArrayState(>[]); @@ -45,7 +41,7 @@ export class UmbCollectionContext< this._entityKey = entityKey; if (storeAlias) { - new UmbContextConsumerController(this._host, storeAlias, (_instance: StoreType) => { + new UmbContextConsumerController(this._host, storeAlias, (_instance) => { this._store = _instance; if (!this._store) { // TODO: if we keep the type assumption of _store existing, then we should here make sure to break the application in a good way. @@ -174,4 +170,4 @@ export class UmbCollectionContext< } } -export const UMB_COLLECTION_CONTEXT_TOKEN = new UmbContextToken>('UmbCollectionContext'); +export const UMB_COLLECTION_CONTEXT_TOKEN = new UmbContextToken>('UmbCollectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts index 5c21bbee38..2c96968191 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts @@ -25,7 +25,7 @@ export class UmbDashboardCollectionElement extends UmbLitElement { ]; // TODO: Use the right type here: - private _collectionContext?: UmbCollectionContext; + private _collectionContext?: UmbCollectionContext; public manifest!: ManifestDashboardCollection; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts index 7045ed5d56..64d73d8412 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts @@ -1,9 +1,10 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing, TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; + import { UmbContextDebugRequest } from '@umbraco-cms/backoffice/context-api'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONTEXT_DEBUGGER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbModalContext, UMB_CONTEXT_DEBUGGER_MODAL, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; @customElement('umb-debug') export class UmbDebug extends UmbLitElement { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts index ff7e0176de..4ef98f15eb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.element.ts @@ -4,7 +4,11 @@ import type { TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { map } from 'rxjs'; import { repeat } from 'lit/directives/repeat.js'; -import { createExtensionElement, isManifestElementableType, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { + createExtensionElement, + isManifestElementableType, + umbExtensionsRegistry, +} from '@umbraco-cms/backoffice/extensions-api'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; export type InitializedExtension = { alias: string; weight: number; component: HTMLElement | null }; @@ -34,6 +38,16 @@ export class UmbExtensionSlotElement extends UmbLitElement { @property({ type: Object, attribute: false }) public filter: (manifest: any) => boolean = () => true; + private _props?: Record = {}; + @property({ type: Object, attribute: false }) + get props() { + return this._props; + } + set props(newVal) { + this._props = newVal; + this.#assignPropsToAllComponents(); + } + @property({ type: String, attribute: 'default-element' }) public defaultElement = ''; @@ -77,6 +91,7 @@ export class UmbExtensionSlotElement extends UmbLitElement { // TODO: Lets make an console.error in this case? } if (component) { + this.#assignProps(component); (component as any).manifest = extension; extensionObject.component = component; @@ -95,6 +110,18 @@ export class UmbExtensionSlotElement extends UmbLitElement { ); } + #assignPropsToAllComponents() { + this._extensions.forEach((ext) => this.#assignProps(ext.component)); + } + + #assignProps = (component: HTMLElement | null) => { + if (!component || !this._props) return; + + Object.keys(this._props).forEach((key) => { + (component as any)[key] = this._props?.[key]; + }); + }; + render() { // TODO: check if we can use repeat directly. return repeat( 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 6bdba312a4..7ba6b5fb07 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 @@ -21,20 +21,26 @@ import './input-checkbox-list/input-checkbox-list.element'; import './input-color-picker/input-color-picker.element'; import './input-culture-select/input-culture-select.element'; import './input-document-picker/input-document-picker.element'; +import './input-document-type-picker/input-document-type-picker.element'; import './input-eye-dropper/input-eye-dropper.element'; import './input-language-picker/input-language-picker.element'; import './input-media-picker/input-media-picker.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; import './input-toggle/input-toggle.element'; +import './input-template-picker/input-template-picker.element'; import './property-type-based-property/property-type-based-property.element'; import './ref-property-editor-ui/ref-property-editor-ui.element'; import './section/section-main/section-main.element'; import './section/section-sidebar/section-sidebar.element'; import './section/section.element'; + import './table/table.element'; + import './tree/tree.element'; +import './tree/entity-tree-item/entity-tree-item.element'; import './tree/tree-menu-item/tree-menu-item.element'; + import './variantable-property/variantable-property.element'; import './workspace/workspace-action-menu/workspace-action-menu.element'; @@ -45,8 +51,9 @@ import './history/history-item.element'; import './workspace/workspace-action/workspace-action.element'; import './workspace/workspace-layout/workspace-layout.element'; -import './code-editor'; - import './workspace/workspace-footer-layout/workspace-footer-layout.element'; +import './template-card/template-card.element'; +import './code-editor'; + export const manifests = [...debugManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts new file mode 100644 index 0000000000..b30071bad0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts @@ -0,0 +1,154 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { + UmbDocumentTypeTreeStore, + UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN, +} from '../../../documents/document-types/repository/document-type.tree.store'; +import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm'; +import { UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN } from '../../../documents/documents/modals/document-type-picker'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { DocumentTypeResponseModel, EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; +import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-input-document-type-picker') +export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitElement) { + static styles = [ + UUITextStyles, + css` + #add-button { + width: 100%; + } + + #current-node { + background-color: var(--uui-color-surface-alt); + } + + #wrapper-nodes { + margin-left: var(--uui-size-space-6); + } + `, + ]; + + // TODO: do we need both selectedKeys and value? If we just use value we follow the same pattern as native form controls. + private _selectedKeys: Array = []; + public get selectedKeys(): Array { + return this._selectedKeys; + } + public set selectedKeys(keys: Array) { + this._selectedKeys = keys; + super.value = keys.join(','); + this._observePickedDocuments(); + } + + @property() + public set value(keysString: string) { + if (keysString !== this._value) { + this.selectedKeys = keysString.split(/[ ,]+/); + } + } + + @property() + currentDocumentType?: DocumentTypeResponseModel; + + @state() + private _items?: Array; + + private _modalContext?: UmbModalContext; + private _documentTypeStore?: UmbDocumentTypeTreeStore; + private _pickedItemsObserver?: UmbObserverController; + + constructor() { + super(); + this.consumeContext(UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this._documentTypeStore = instance; + this._observePickedDocuments(); + }); + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + } + + protected getFormElement() { + return undefined; + } + + private _observePickedDocuments() { + this._pickedItemsObserver?.destroy(); + + if (!this._documentTypeStore) return; + + // TODO: consider changing this to the list data endpoint when it is available + this._pickedItemsObserver = this.observe(this._documentTypeStore.items(this._selectedKeys), (items) => { + this._items = items; + }); + } + + private _openPicker() { + // We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data: + const modalHandler = this._modalContext?.open(UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN, { + multiple: true, + selection: [...this._selectedKeys], + }); + + modalHandler?.onSubmit().then(({ selection }: any) => { + this._setSelection(selection); + }); + } + + private async _removeItem(item: DocumentTypeResponseModel) { + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { + color: 'danger', + headline: `Remove ${item.name}?`, + content: 'Are you sure you want to remove this item', + confirmLabel: 'Remove', + }); + + await modalHandler?.onSubmit(); + const newSelection = this._selectedKeys.filter((value) => value !== item.key); + this._setSelection(newSelection); + } + + private _setSelection(newSelection: Array) { + this.selectedKeys = newSelection; + this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + } + + render() { + return html` + + + +
+ ${this._items?.map((item) => this._renderItem(item))} + Add +
+ `; + } + + private _renderItem(item: DocumentTypeResponseModel) { + // TODO: remove when we have a way to handle trashed items + const tempItem = item as DocumentTypeResponseModel & { isTrashed: boolean }; + + return html` + + + ${tempItem.isTrashed ? html` Trashed ` : nothing} + + this._removeItem(item)} label="Remove document ${item.name}">Remove + + + `; + } +} + +export default UmbInputDocumentTypePickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-document-type-picker': UmbInputDocumentTypePickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts new file mode 100644 index 0000000000..ca6480f1db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts @@ -0,0 +1,194 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UmbTemplateCardElement } from '../template-card/template-card.element'; +import { UMB_TEMPLATE_PICKER_MODAL_TOKEN } from '../../modals/template-picker'; +import { UMB_TEMPLATE_MODAL_TOKEN } from '../../modals/template'; +import { UmbTemplateRepository } from '../../../templating/templates/repository/template.repository'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-input-template-picker') +export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElement) { + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default undefined + */ + @property({ type: Number }) + min?: number; + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default undefined + */ + @property({ type: Number }) + max?: number; + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + _allowedKeys: Array = []; + @property({ type: Array }) + public get allowedKeys() { + return this._allowedKeys; + } + public set allowedKeys(newKeys: Array) { + this._allowedKeys = newKeys; + this.#observePickedTemplates(); + } + + _defaultKey = ''; + @property({ type: String }) + public get defaultKey(): string { + return this._defaultKey; + } + public set defaultKey(newKey: string) { + this._defaultKey = newKey; + super.value = newKey; + } + + private _modalContext?: UmbModalContext; + private _templateRepository: UmbTemplateRepository = new UmbTemplateRepository(this); + + @state() + _pickedTemplates: TemplateResponseModel[] = []; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + } + + async #observePickedTemplates() { + this.observe( + await this._templateRepository.treeItems(this._allowedKeys), + (data) => { + this._pickedTemplates = data; + }, + '_templateRepositoryTreeItems' + ); + } + + protected getFormElement() { + return this; + } + + #changeDefault(e: CustomEvent) { + e.stopPropagation(); + const newKey = (e.target as UmbTemplateCardElement).value as string; + this.defaultKey = newKey; + this.dispatchEvent(new CustomEvent('change-default')); + } + + #openPicker() { + const modalHandler = this._modalContext?.open(UMB_TEMPLATE_PICKER_MODAL_TOKEN, { + multiple: true, + selection: [...this.allowedKeys], + }); + + modalHandler?.onSubmit().then((data) => { + if (!data.selection) return; + this.allowedKeys = data.selection; + this.dispatchEvent(new CustomEvent('change-allowed')); + }); + } + + #removeTemplate(key: string) { + /* + TODO: We need to follow up on this experience. + Could we test if this document type is in use, if so we should have a dialog notifying the user(Dialog, are you sure...) about that we might will break something? + If thats the case, Im not why if a template will be removed from an actual document. + If if its just the option that will go away. + (Comment by Niels) + In current backoffice we just prevent deleting a default when there are other templates. But if its the only one its okay. This is a weird experience, so we should make something that makes more sense. + BTW. its weird cause the damage of removing the default template is equally bad when there is one or more templates. + */ + this.allowedKeys = this.allowedKeys.filter((x) => x !== key); + } + + #openTemplate(e: CustomEvent) { + const key = (e.target as UmbTemplateCardElement).value; + + this._modalContext?.open(UMB_TEMPLATE_MODAL_TOKEN, { + key: key as string, + language: 'razor', + }); + } + + render() { + return html` + ${this._pickedTemplates.map( + (template) => html` + + + + + + ` + )} + Add + `; + } + + static styles = [ + UUITextStyles, + css` + #add-button { + width: 100%; + } + :host { + box-sizing: border-box; + display: flex; + gap: var(--uui-size-space-4); + flex-wrap: wrap; + } + + :host > * { + max-width: 180px; + min-width: 180px; + min-height: 150px; + } + `, + ]; +} + +export default UmbInputTemplatePickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-template-picker': UmbInputTemplatePickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts index 66f502e948..2f90f58eca 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts @@ -13,16 +13,6 @@ export class UmbSectionContext { public readonly pathname = this.#manifestPathname.asObservable(); public readonly label = this.#manifestLabel.asObservable(); - /* - This was not used anywhere - private _activeTree = new BehaviorSubject(undefined); - public readonly activeTree = this._activeTree.asObservable(); - */ - - // TODO: what is the best context to put this in? - #activeTreeItem = new ObjectState(undefined); - public readonly activeTreeItem = this.#activeTreeItem.asObservable(); - constructor(manifest: ManifestSection) { this.setManifest(manifest); } @@ -32,17 +22,6 @@ export class UmbSectionContext { this.#manifestPathname.next(manifest?.meta?.pathname); this.#manifestLabel.next(manifest ? manifest.meta?.label || manifest.name : undefined); } - - /* - This was not used anywhere - public setActiveTree(tree: ManifestTree) { - this._activeTree.next(tree); - } - */ - - public setActiveTreeItem(item?: ActiveTreeItemType) { - this.#activeTreeItem.next(item); - } } export const UMB_SECTION_CONTEXT_TOKEN = new UmbContextToken('UmbSectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts new file mode 100644 index 0000000000..50f8cc5484 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts @@ -0,0 +1,170 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +/** + * @element umb-template-card + * @slot actions + * @fires open + * @fires selected + * + * + */ + +@customElement('umb-template-card') +export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) { + static styles = [ + UUITextStyles, + css` + :host { + box-sizing: border-box; + display: contents; + position: relative; + + height: 100%; + border: 1px solid red; + margin: auto; + } + + #card { + box-sizing: border-box; + width: 100%; + max-width: 180px; + //width: 200px; + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-divider-emphasis); + background-color: var(--uui-color-background); + padding: var(--uui-size-4); + } + + :host([default]) #card { + border: 1px solid var(--uui-color-selected); + outline: 1px solid var(--uui-color-selected); + } + #card:has(uui-button:hover) { + border: 1px solid var(--uui-color-selected); + } + + #bottom { + margin-top: auto; + } + + slot[name='actions'] { + position: absolute; + top: var(--uui-size-4); + right: var(--uui-size-4); + display: flex; + justify-content: right; + + opacity: 0; + transition: opacity 120ms; + } + + :host(:focus) slot[name='actions'], + :host(:focus-within) slot[name='actions'], + :host(:hover) slot[name='actions'] { + opacity: 1; + } + + #open-part { + border: none; + outline: none; + background: none; + text-align: center; + display: flex; + flex-direction: column; + font-weight: 700; + align-items: center; + cursor: pointer; + flex-grow: 1; + } + + #open-part, + #card { + gap: var(--uui-size-space-2); + } + + #open-part strong { + flex-grow: 1; + display: flex; + align-items: center; + } + + :host([disabled]) #open-part { + pointer-events: none; + } + + #open-part:focus-visible, + #open-part:focus-visible uui-icon, + #open-part:hover, + #open-part:hover uui-icon { + text-decoration: underline; + color: var(--uui-color-interactive-emphasis); + } + + #open-part uui-icon { + font-size: var(--uui-size-20); + color: var(--uui-color-divider-emphasis); + } + `, + ]; + + @property({ type: String }) + name = ''; + + @property({ type: Boolean, reflect: true }) + default = false; + + _key = ''; + @property({ type: String }) + public set key(newKey: string) { + this._key = newKey; + super.value = newKey; + } + public get key() { + return this._key; + } + + protected getFormElement() { + return undefined; + } + + #setSelection(e: KeyboardEvent) { + e.preventDefault(); + e.stopPropagation(); + //this.selected = true; + this.dispatchEvent(new CustomEvent('change-default', { bubbles: true, composed: true })); + } + #openTemplate(e: KeyboardEvent) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('open', { bubbles: true, composed: true })); + } + + render() { + return html`
+ + + ${this.default ? '(Default template)' : 'Set default'} + + +
`; + } +} + +export default UmbTemplateCardElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-template-card': UmbTemplateCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.stories.ts new file mode 100644 index 0000000000..464e9bc44b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.stories.ts @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import './template-card.element'; +import type { UmbTemplateCardElement } from './template-card.element'; + +const meta: Meta = { + title: 'Components/Template Card', + component: 'umb-template-card', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + name: 'Template with a name ', + }, +}; + +export const Default: Story = { + args: { + name: 'Just a template', + }, +}; + +export const LongName: Story = { + args: { + name: 'Another template that someone gave a way way too long name without really thinking twice about it', + }, +}; + +export const TemplateCardList: Story = { + render: () => html`
+ + + + +
`, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.context.ts new file mode 100644 index 0000000000..2b014d81c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.context.ts @@ -0,0 +1,10 @@ +import { UmbTreeItemContextBase } from '../tree-item-base/tree-item-base.context'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO get unique method from an entity repository static method +export class UmbEntityTreeItemContext extends UmbTreeItemContextBase { + constructor(host: UmbControllerHostInterface) { + super(host, (x: EntityTreeItemResponseModel) => x.key); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.element.ts new file mode 100644 index 0000000000..11fd1f40f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/entity-tree-item/entity-tree-item.element.ts @@ -0,0 +1,49 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbEntityTreeItemContext } from './entity-tree-item.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { ManifestKind } from '@umbraco-cms/backoffice/extensions-registry'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; + +// TODO: Move to separate file: +const manifest: ManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityTreeItem', + matchKind: 'entity', + matchType: 'treeItem', + manifest: { + type: 'treeItem', + elementName: 'umb-entity-tree-item', + }, +}; +umbExtensionsRegistry.register(manifest); + +@customElement('umb-entity-tree-item') +export class UmbEntityTreeItemElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + private _item?: EntityTreeItemResponseModel; + @property({ type: Object, attribute: false }) + public get item() { + return this._item; + } + public set item(value: EntityTreeItemResponseModel | undefined) { + this._item = value; + this.#context.setTreeItem(value); + } + + #context = new UmbEntityTreeItemContext(this); + + render() { + if (!this.item) return nothing; + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-tree-item': UmbEntityTreeItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts new file mode 100644 index 0000000000..7216fb0683 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts @@ -0,0 +1,185 @@ +import { map } from 'rxjs'; +import { + UmbSectionSidebarContext, + UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, +} from '../../section/section-sidebar/section-sidebar.context'; +import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../../section/section.context'; +import { UmbTreeContextBase } from '../tree.context'; +import { UmbTreeItemContext } from '../tree-item.context.interface'; +import { BooleanState, DeepState, StringState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { + UmbContextConsumerController, + UmbContextProviderController, + UmbContextToken, +} from '@umbraco-cms/backoffice/context-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import type { TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; + +// add type for unique function +export type UmbTreeItemUniqueFunction = (x: T) => string | null | undefined; + +export class UmbTreeItemContextBase + implements UmbTreeItemContext +{ + public host: UmbControllerHostInterface; + public unique?: string; + public type?: string; + + #treeItem = new DeepState(undefined); + treeItem = this.#treeItem.asObservable(); + + #hasChildren = new BooleanState(false); + hasChildren = this.#hasChildren.asObservable(); + + #isLoading = new BooleanState(false); + isLoading = this.#isLoading.asObservable(); + + #isSelectable = new BooleanState(false); + isSelectable = this.#isSelectable.asObservable(); + + #isSelected = new BooleanState(false); + isSelected = this.#isSelected.asObservable(); + + #isActive = new BooleanState(false); + isActive = this.#isActive.asObservable(); + + #hasActions = new BooleanState(false); + hasActions = this.#hasActions.asObservable(); + + #path = new StringState(''); + path = this.#path.asObservable(); + + treeContext?: UmbTreeContextBase; + #sectionContext?: UmbSectionContext; + #sectionSidebarContext?: UmbSectionSidebarContext; + #getUniqueFunction: UmbTreeItemUniqueFunction; + #actionObserver?: UmbObserverController; + + constructor(host: UmbControllerHostInterface, getUniqueFunction: UmbTreeItemUniqueFunction) { + this.host = host; + this.#getUniqueFunction = getUniqueFunction; + this.#consumeContexts(); + new UmbContextProviderController(host, UMB_TREE_ITEM_CONTEXT_TOKEN, this); + } + + public setTreeItem(treeItem: T | undefined) { + if (!treeItem) { + this.#treeItem.next(undefined); + return; + } + + const unique = this.#getUniqueFunction(treeItem); + if (!unique) throw new Error('Could not create tree item context, unique key is missing'); + this.unique = unique; + + if (!treeItem.type) throw new Error('Could not create tree item context, tree item type is missing'); + this.type = treeItem.type; + + this.#hasChildren.next(treeItem.hasChildren || false); + this.#observeActions(); + this.#treeItem.next(treeItem); + } + + public async requestChildren() { + if (!this.unique) throw new Error('Could not request children, unique key is missing'); + + // TODO: wait for tree context to be ready + this.#isLoading.next(true); + const response = await this.treeContext!.requestChildrenOf(this.unique); + this.#isLoading.next(false); + return response; + } + + public toggleContextMenu() { + if (!this.getTreeItem() || !this.type || this.unique === undefined) { + throw new Error('Could not request children, tree item is not set'); + } + + this.#sectionSidebarContext?.toggleContextMenu(this.type, this.unique, this.getTreeItem()?.name || ''); + } + + public select() { + if (!this.unique) throw new Error('Could not request children, unique key is missing'); + this.treeContext?.select(this.unique); + } + + public deselect() { + if (!this.unique) throw new Error('Could not request children, unique key is missing'); + this.treeContext?.deselect(this.unique); + } + + #consumeContexts() { + new UmbContextConsumerController(this.host, UMB_SECTION_CONTEXT_TOKEN, (instance) => { + this.#sectionContext = instance; + this.#observeSectionPath(); + }); + + new UmbContextConsumerController(this.host, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, (instance) => { + this.#sectionSidebarContext = instance; + }); + + new UmbContextConsumerController(this.host, 'umbTreeContext', (treeContext: UmbTreeContextBase) => { + this.treeContext = treeContext; + this.#observeIsSelectable(); + this.#observeIsSelected(); + }); + } + + getTreeItem() { + return this.#treeItem.getValue(); + } + + #observeIsSelectable() { + if (!this.treeContext) return; + new UmbObserverController(this.host, this.treeContext.selectable, (value) => this.#isSelectable.next(value)); + } + + #observeIsSelected() { + if (!this.treeContext) throw new Error('Could not request children, tree context is missing'); + if (!this.unique) throw new Error('Could not request children, unique key is missing'); + + new UmbObserverController( + this.host, + this.treeContext.selection.pipe(map((selection) => selection.includes(this.unique!))), + (isSelected) => { + this.#isSelected.next(isSelected); + } + ); + } + + #observeSectionPath() { + if (!this.#sectionContext) return; + + new UmbObserverController(this.host, this.#sectionContext.pathname, (pathname) => { + if (!pathname) return; + if (!this.type) throw new Error('Cant construct path, entity type is missing'); + if (!this.unique) throw new Error('Cant construct path, unique is missing'); + + const path = this.constructPath(pathname, this.type, this.unique); + this.#path.next(path); + }); + } + + #observeActions() { + if (this.#actionObserver) this.#actionObserver.destroy(); + + this.#actionObserver = new UmbObserverController( + this.host, + umbExtensionsRegistry + .extensionsOfType('entityAction') + .pipe(map((actions) => actions.filter((action) => action.conditions.entityType === this.type))), + (actions) => { + this.#hasActions.next(actions.length > 0); + } + ); + } + + // TODO: use router context + constructPath(pathname: string, entityType: string, unique: string) { + return `section/${pathname}/workspace/${entityType}/edit/${unique}`; + } +} + +export const UMB_TREE_ITEM_CONTEXT_TOKEN = new UmbContextToken('UmbTreeItemContext'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.element.ts new file mode 100644 index 0000000000..a2b059421c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.element.ts @@ -0,0 +1,168 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { UmbTreeItemContext } from '../tree-item.context.interface'; +import { UMB_TREE_ITEM_CONTEXT_TOKEN } from './tree-item-base.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-tree-item-base') +export class UmbTreeItemBaseElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + @state() + private _item?: TreeItemPresentationModel; + + @state() + private _childItems?: TreeItemPresentationModel[]; + + @state() + private _href?: string; + + @state() + private _isLoading = false; + + @state() + private _isSelectable = false; + + @state() + private _isSelected = false; + + @state() + private _hasActions = false; + + @state() + private _hasChildren = false; + + @state() + private _iconSlotHasChildren = false; + + #treeItemContext?: UmbTreeItemContext; + + constructor() { + super(); + + this.consumeContext(UMB_TREE_ITEM_CONTEXT_TOKEN, (instance) => { + this.#treeItemContext = instance; + if (!this.#treeItemContext) return; + // TODO: investigate if we can make an observe decorator + this.observe(this.#treeItemContext.treeItem, (value) => (this._item = value)); + this.observe(this.#treeItemContext.hasChildren, (value) => (this._hasChildren = value)); + this.observe(this.#treeItemContext.isLoading, (value) => (this._isLoading = value)); + this.observe(this.#treeItemContext.isSelectable, (value) => (this._isSelectable = value)); + this.observe(this.#treeItemContext.isSelected, (value) => (this._isSelected = value)); + this.observe(this.#treeItemContext.hasActions, (value) => (this._hasActions = value)); + this.observe(this.#treeItemContext.path, (value) => (this._href = value)); + }); + } + + connectedCallback(): void { + super.connectedCallback(); + this.addEventListener('selected', this._handleSelectedItem); + this.addEventListener('unselected', this._handleDeselectedItem); + } + + private _handleSelectedItem(event: Event) { + event.stopPropagation(); + this.#treeItemContext?.select(); + } + + private _handleDeselectedItem(event: Event) { + event.stopPropagation(); + this.#treeItemContext?.deselect(); + } + + // TODO: do we want to catch and emit a backoffice event here? + private _onShowChildren() { + if (this._childItems && this._childItems.length > 0) return; + this.#observeChildren(); + } + + async #observeChildren() { + if (!this.#treeItemContext?.requestChildren) return; + + const { asObservable } = await this.#treeItemContext.requestChildren(); + if (!asObservable) return; + + this.observe(asObservable(), (childItems: any) => { + this._childItems = childItems; + }); + } + + private _openActions() { + this.#treeItemContext?.toggleContextMenu(); + } + + render() { + return html` + + ${this.#renderIcon()} ${this.#renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()} + + + `; + } + + #hasNodes = (e: Event) => { + return (e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0; + }; + + #renderIcon() { + return html` + { + this._iconSlotHasChildren = this.#hasNodes(e); + }}> + ${this._item?.icon && !this._iconSlotHasChildren + ? html` ` + : nothing} + `; + } + + #renderLabel() { + return html``; + } + + #renderActions() { + return html` + ${this._hasActions + ? html` + + + + + + ` + : nothing} + `; + } + + #renderChildItems() { + return html` + ${this._childItems + ? repeat( + this._childItems, + // TODO: get unique here instead of name. we might be able to get it from the context + (item) => item.name, + (item) => html`` + ) + : ''} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree-item-base': UmbTreeItemBaseElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.stories.ts new file mode 100644 index 0000000000..f81fe83f46 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.stories.ts @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './tree-item-base.element'; +import type { UmbTreeItemBaseElement } from './tree-item-base.element'; + +// TODO: provide tree item context to make this element render properly +const meta: Meta = { + title: 'Components/Tree/Tree Item', + component: 'umb-tree-item', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: {}, +}; + +export const WithChildren: Story = { + args: {}, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.context.interface.ts new file mode 100644 index 0000000000..0144422f76 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.context.interface.ts @@ -0,0 +1,35 @@ +import { Observable } from 'rxjs'; +import { ProblemDetailsModel, TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostInterface } from 'libs/controller/controller-host.mixin'; + +// TODO: temp type. Add paged response type to the repository interface +interface PagedResponse { + total: number; + items: Array; +} + +export interface UmbTreeItemContext { + host: UmbControllerHostInterface; + unique?: string; + type?: string; + + treeItem: Observable; + hasChildren: Observable; + isLoading: Observable; + isSelectable: Observable; + isSelected: Observable; + isActive: Observable; + hasActions: Observable; + path: Observable; + + setTreeItem(treeItem: T | undefined): void; + requestChildren(): Promise<{ + data: PagedResponse | undefined; + error: ProblemDetailsModel | undefined; + asObservable?: () => Observable; + }>; + toggleContextMenu(): void; + select(): void; + deselect(): void; + constructPath(pathname: string, entityType: string, unique: string): string; +} 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 deleted file mode 100644 index 6f9eb57709..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { css, html, nothing } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, state } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { map, Observable } from 'rxjs'; -import { repeat } from 'lit/directives/repeat.js'; -import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section/section.context'; -import { - UmbSectionSidebarContext, - UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, -} from '../section/section-sidebar/section-sidebar.context'; -import type { UmbTreeContextBase } from './tree.context'; -import type { Entity } from '@umbraco-cms/backoffice/models'; -import type { UmbTreeStore } from '@umbraco-cms/backoffice/store'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; - -// TODO: align menu items and tree items -@customElement('umb-tree-item') -export class UmbTreeItem extends UmbLitElement { - static styles = [UUITextStyles, css``]; - - @property({ type: String }) - key = ''; - - @property({ type: String }) - parentKey: string | null = null; - - @property({ type: String }) - label = ''; - - @property({ type: String }) - icon = ''; - - private _entityType = ''; - @property({ type: String }) - get entityType() { - return this._entityType; - } - set entityType(newVal) { - const oldVal = this._entityType; - this._entityType = newVal; - this.requestUpdate('entityType', oldVal); - this._observeEntityActions(); - } - - @property({ type: Boolean, attribute: 'has-children' }) - hasChildren = false; - - @state() - private _childItems?: Entity[]; - - @state() - private _href?: string; - - @state() - private _loading = false; - - @state() - private _selectable = false; - - @state() - private _selected = false; - - @state() - private _isActive = false; - - @state() - private _hasActions = false; - - private _treeContext?: UmbTreeContextBase; - private _store?: UmbTreeStore; - private _sectionContext?: UmbSectionContext; - private _sectionSidebarContext?: UmbSectionSidebarContext; - - constructor() { - super(); - - this.consumeContext('umbTreeContext', (treeContext: UmbTreeContextBase) => { - this._treeContext = treeContext; - this._observeSelectable(); - this._observeIsSelected(); - }); - - this.consumeContext('umbStore', (store: UmbTreeStore) => { - this._store = store; - }); - - this.consumeContext(UMB_SECTION_CONTEXT_TOKEN, (sectionContext) => { - this._sectionContext = sectionContext; - this._observeSection(); - this._observeActiveTreeItem(); - }); - - this.consumeContext(UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, (instance) => { - this._sectionSidebarContext = instance; - }); - } - - connectedCallback(): void { - super.connectedCallback(); - - this.addEventListener('selected', this._handleSelectedItem); - this.addEventListener('unselected', this._handleDeselectedItem); - } - - private _handleSelectedItem(event: Event) { - event.stopPropagation(); - this._treeContext?.select(this.key); - } - - private _handleDeselectedItem(event: Event) { - event.stopPropagation(); - this._treeContext?.deselect(this.key); - } - - private _observeSection() { - if (!this._sectionContext) return; - - this.observe(this._sectionContext?.pathname, (pathname) => { - this._href = this._constructPath(pathname || '', this.entityType, this.key); - }); - } - - private _observeSelectable() { - if (!this._treeContext) return; - - this.observe(this._treeContext.selectable, (value) => { - this._selectable = value || false; - }); - } - - private _observeIsSelected() { - if (!this._treeContext) return; - - this.observe(this._treeContext.selection.pipe(map((keys) => keys?.includes(this.key))), (isSelected) => { - this._selected = isSelected || false; - }); - } - - private _observeActiveTreeItem() { - if (!this._sectionContext) return; - - this.observe(this._sectionContext?.activeTreeItem, (treeItem) => { - this._isActive = this.key === treeItem?.key; - }); - } - - private _observeEntityActions() { - // TODO: Stop previous observation, currently we can do this from the UmbElementMixin as its a new subscription when Actions or entityType has changed. - // Solution: store the current observation controller and if it existing then destroy it. - // TODO: as long as a tree consist of one entity type we don't have to observe this every time a new tree item is created. - // Solution: move this to the tree context and observe it once. - this.observe( - umbExtensionsRegistry - .extensionsOfType('entityAction') - .pipe(map((actions) => actions.filter((action) => action.conditions.entityType === this._entityType))), - (actions) => { - this._hasActions = actions.length > 0; - } - ); - } - - // TODO: how do we handle this? - private _constructPath(sectionPathname: string, type: string, key: string) { - return type ? `section/${sectionPathname}/workspace/${type}/edit/${key}` : undefined; - } - - // TODO: do we want to catch and emit a backoffice event here? - 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; - - this._loading = true; - - // TODO: we should do something about these types, stop having our own version of Entity. - this.observe(this._store.getTreeItemChildren(this.key) as Observable, (childItems) => { - this._childItems = childItems; - this._loading = false; - }); - } - - private _openActions() { - if (!this._treeContext || !this._sectionContext) return; - - // This is out-commented as it was not used. only kept if someone need this later: - //this._sectionContext?.setActiveTree(this._treeContext?.tree); - - this._sectionContext?.setActiveTreeItem({ - key: this.key, - name: this.label, - icon: this.icon, - type: this.entityType, - hasChildren: this.hasChildren, - parentKey: this.parentKey, - }); - this._sectionSidebarContext?.toggleContextMenu(this.entityType, this.key, this.label); - } - - render() { - return html` - - ${this._renderChildItems()} - - ${this._renderActions()} - - - `; - } - - private _renderChildItems() { - return html` - ${this._childItems - ? repeat( - this._childItems, - (item) => item.key, - (item) => - html`` - ) - : ''} - `; - } - - private _renderActions() { - return html` - ${this._hasActions - ? html` - - - - - - ` - : nothing} - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'umb-tree-item': UmbTreeItem; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts deleted file mode 100644 index 5b59cbde3b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Meta, StoryObj } from '@storybook/web-components'; -import './tree-item.element'; -import type { UmbTreeItem } from './tree-item.element' - -const meta: Meta = { - title: 'Components/Tree/Tree Item', - component: 'umb-tree-item', -}; - -export default meta; -type Story = StoryObj; - -export const Overview: Story = { - args: { - label: 'My Tree Item', - icon: 'umb:home', - hasChildren: false, - } -}; - -export const WithChildren: Story = { - args: { - label: 'My Tree Item', - icon: 'umb:home', - hasChildren: true, - } -}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item/tree-item.element.ts new file mode 100644 index 0000000000..9cecba3412 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item/tree-item.element.ts @@ -0,0 +1,30 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { TreeItemPresentationModel } from 'libs/backend-api/src'; +import { ManifestTreeItem } from 'libs/extensions-registry/tree-item.models'; + +@customElement('umb-tree-item') +export class UmbTreeItemElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + @property({ type: Object, attribute: false }) + item?: TreeItemPresentationModel; + + render() { + if (!this.item) return nothing; + return html` manifests.conditions.entityType === this.item?.type} + .props=${{ + item: this.item, + }}>`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree-item': UmbTreeItemElement; + } +} 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 d01a1dc0ff..954cdb58ab 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,8 +1,10 @@ import type { Observable } from 'rxjs'; import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; -import { DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { DeepState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; export interface UmbTreeContext { tree: ManifestTree; @@ -14,7 +16,7 @@ export interface UmbTreeContext { } export class UmbTreeContextBase implements UmbTreeContext { - #host: UmbControllerHostInterface; + host: UmbControllerHostInterface; public tree: ManifestTree; #selectable = new DeepState(false); @@ -23,15 +25,44 @@ export class UmbTreeContextBase implements UmbTreeContext { #selection = new DeepState(>[]); public readonly selection = this.#selection.asObservable(); - repository!: UmbTreeRepository; + repository?: UmbTreeRepository; + + #initResolver?: () => void; + #initialized = false; + + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); constructor(host: UmbControllerHostInterface, tree: ManifestTree) { - this.#host = host; + this.host = host; this.tree = tree; - if (this.tree.meta.repository) { - // TODO: should be using the right extension and the createExtensionClass method. - this.repository = new this.tree.meta.repository(this.#host) as any; + const repositoryAlias = this.tree.meta.repositoryAlias; + if (!repositoryAlias) throw new Error('Tree must have a repository alias.'); + + new UmbObserverController( + this.host, + umbExtensionsRegistry.getByTypeAndAlias('repository', this.tree.meta.repositoryAlias), + async (repositoryManifest) => { + if (!repositoryManifest) return; + + try { + const result = await createExtensionClass(repositoryManifest, [this.host]); + this.repository = result; + this.#checkIfInitialized(); + } catch (error) { + throw new Error('Could not create repository with alias: ' + repositoryAlias + ''); + } + } + ); + } + + // TODO: find a generic way to do this + #checkIfInitialized() { + if (this.repository) { + this.#initialized = true; + this.#initResolver?.(); } } @@ -58,18 +89,22 @@ export class UmbTreeContextBase implements UmbTreeContext { } public async requestRootItems() { - return this.repository.requestRootTreeItems(); + await this.#init; + return this.repository!.requestRootTreeItems(); } public async requestChildrenOf(parentKey: string | null) { - return this.repository.requestTreeItemsOf(parentKey); + await this.#init; + return this.repository!.requestTreeItemsOf(parentKey); } public async rootItems() { - return this.repository.rootTreeItems(); + await this.#init; + return this.repository!.rootTreeItems(); } public async childrenOf(parentKey: string | null) { - return this.repository.treeItemsOf(parentKey); + await this.#init; + return this.repository!.treeItemsOf(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 e5e5d1c5a7..3466abc60b 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 @@ -3,12 +3,13 @@ import { customElement, property, state } from 'lit/decorators.js'; import { map } from 'rxjs'; import { repeat } from 'lit/directives/repeat.js'; import { UmbTreeContextBase } from './tree.context'; -import type { Entity } from '@umbraco-cms/backoffice/models'; import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import './tree-item.element'; +import './tree-item/tree-item.element'; +import './tree-item-base/tree-item-base.element'; import './context-menu/tree-context-menu-page.service'; import './context-menu/tree-context-menu.service'; @@ -54,10 +55,7 @@ export class UmbTreeElement extends UmbLitElement { private _tree?: ManifestTree; @state() - private _items: Entity[] = []; - - @state() - private _loading = true; + private _items: EntityTreeItemResponseModel[] = []; private _treeContext?: UmbTreeContextBase; @@ -101,7 +99,7 @@ export class UmbTreeElement extends UmbLitElement { this._treeContext.requestRootItems(); this.observe(await this._treeContext.rootItems(), (rootItems) => { - this._items = rootItems as Entity[]; + this._items = rootItems; }); } @@ -119,15 +117,9 @@ export class UmbTreeElement extends UmbLitElement { return html` ${repeat( this._items, - (item) => item.key, - (item) => - html`` + // TODO: add getUnique to a repository interface + (item, index) => index, + (item) => html`` )} `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts index a5a5be1fec..b6c42e1de1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts @@ -30,7 +30,7 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement { private _workspaceContext?: UmbEntityWorkspaceContextInterface; // TODO: add type for the collection context. - private _collectionContext?: UmbCollectionContext; + private _collectionContext?: UmbCollectionContext; constructor() { super(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts index e066a7ec1a..a06b85f015 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts @@ -1,4 +1,4 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface'; +import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; export interface UmbEntityWorkspaceContextInterface extends UmbWorkspaceContextInterface { getEntityKey(): string | undefined; // COnsider if this should go away now that we have getUnique() diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts index 6b1201a965..bf46f4dc6b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts @@ -32,8 +32,8 @@ export class UmbWorkspacePropertyStructureManager = [ name: 'Section Picker Modal', loader: () => import('./section-picker/section-picker-modal.element'), }, + { + type: 'modal', + alias: 'Umb.Modal.TemplatePicker', + name: 'Template Picker Modal', + loader: () => import('./template-picker/template-picker-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.Template', + name: 'Template Modal', + loader: () => import('./template/template-modal.element'), + }, { type: 'modal', alias: 'Umb.Modal.EmbeddedMedia', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/index.ts new file mode 100644 index 0000000000..f8f2560e3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbTemplatePickerModalData { + multiple: boolean; + selection: string[]; +} + +export interface UmbTemplatePickerModalResult { + selection: string[] | undefined; +} + +export const UMB_TEMPLATE_PICKER_MODAL_TOKEN = new UmbModalToken< + UmbTemplatePickerModalData, + UmbTemplatePickerModalResult +>('Umb.Modal.TemplatePicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/template-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/template-picker-modal.element.ts new file mode 100644 index 0000000000..49a14aa0d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template-picker/template-picker-modal.element.ts @@ -0,0 +1,105 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbTreeElement } from '../../components/tree/tree.element'; +import { UmbTemplatePickerModalData, UmbTemplatePickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; + +//TODO: make a default tree-picker that can be used across multiple pickers +// TODO: make use of UmbPickerLayoutBase +@customElement('umb-template-picker-modal') +export class UmbTemplatePickerModalElement extends UmbModalBaseElement< + UmbTemplatePickerModalData, + UmbTemplatePickerModalResult +> { + @state() + _selection: Array = []; + + @state() + _multiple = true; + + connectedCallback() { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this._multiple = this.data?.multiple ?? true; + } + + private _handleSelectionChange(e: CustomEvent) { + e.stopPropagation(); + const element = e.target as UmbTreeElement; + this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; + } + + private _submit() { + this.modalHandler?.submit({ selection: this._selection }); + } + + private _close() { + this.modalHandler?.reject(); + } + + // TODO: implement search + // TODO: make umb-tree have a disabled option (string array like selection)? + render() { + return html` + + + +
+ +
+
+ + +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + h3 { + margin-left: var(--uui-size-space-5); + margin-right: var(--uui-size-space-5); + } + + uui-input { + width: 100%; + } + + hr { + border: none; + border-bottom: 1px solid var(--uui-color-divider); + margin: 16px 0; + } + + #content-list { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + } + + .content-item { + cursor: pointer; + } + + .content-item.selected { + background-color: var(--uui-color-selected); + color: var(--uui-color-selected-contrast); + } + `, + ]; +} + +export default UmbTemplatePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-template-picker-modal': UmbTemplatePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/index.ts new file mode 100644 index 0000000000..741a6d67b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbTemplateModalData { + key: string; + language?: 'razor' | 'typescript' | 'javascript' | 'css' | 'markdown' | 'json' | 'html'; +} + +export interface UmbTemplateModalResult { + key: string; +} + +export const UMB_TEMPLATE_MODAL_TOKEN = new UmbModalToken( + 'Umb.Modal.Template', + { + type: 'sidebar', + size: 'full', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/template-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/template-modal.element.ts new file mode 100644 index 0000000000..57086d24eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/template/template-modal.element.ts @@ -0,0 +1,162 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, query, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UUIInputEvent } from '@umbraco-ui/uui'; +import { UmbCodeEditor } from '../../components/code-editor'; +import { UmbTemplateModalData, UmbTemplateModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbInputEvent } from 'libs/umb-events/input.event'; +import { TemplateResource, TemplateResponseModel } from 'libs/backend-api/src'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +//TODO: make a default tree-picker that can be used across multiple pickers +// TODO: make use of UmbPickerLayoutBase +@customElement('umb-template-modal') +export class UmbTemplateModalElement extends UmbModalBaseElement { + @state() + _key = ''; + + @state() + _template?: TemplateResponseModel; + + @query('umb-code-editor') + _codeEditor?: UmbCodeEditor; + + connectedCallback() { + super.connectedCallback(); + + if (!this.data?.key) return; + + // TODO: use the template workspace instead of a custom modal. This is still to be made available as infinite editors(Modals). + alert('This should be using the Template Workspace instead of a custom build modal.'); + this._key = this.data.key; + this.#getTemplate(); + } + + async #getTemplate() { + const { data } = await tryExecuteAndNotify(this, TemplateResource.getTemplateByKey({ key: this._key })); + if (!data) return; + + this._template = data; + } + + async #saveTemplate() { + const { error } = await tryExecuteAndNotify( + this, + TemplateResource.putTemplateByKey({ key: this._key, requestBody: this._template }) + ); + if (!error) { + console.log(`template (${this._key}) saved successfully`); + } + } + + private _submit() { + if (!this._template?.key) return; + + this.#saveTemplate(); + this.modalHandler?.submit({ key: this._template.key }); + } + + private _close() { + this.modalHandler?.reject(); + } + + #codeEditorInput(e: UmbInputEvent) { + e.stopPropagation(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this._template.code = this._codeEditor?.code; + } + + #templateNameInput(e: UUIInputEvent) { + if (!this._template) return; + this._template.name = e.target.value as string; + } + + render() { + return html` + +
+ +
${this._template?.alias}
+
+
+ + +
+ Master template: To be continued +
+ To be continued + To be continued + To be continued +
+
+ +
+
+ + +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + uui-box { + position: relative; + display: block; + height: 100%; + margin: var(--uui-size-layout-1); + } + + #layout-header { + display: flex; + width: 100%; + align-items: center; + margin: 0 var(--uui-size-layout-1); + } + + #template-name { + align-items: center; + padding: 0 var(--uui-size-space-3); + flex-grow: 1; + } + + umb-code-editor { + position: absolute; /** Umb-code-editor has issues with height, this is the temp solution on this case */ + top: 75px; + left: 0; + bottom: 0; + width: 100%; + } + + #button-group { + display: flex; + justify-content: space-between; + } + + #secondary-group { + display: flex; + gap: var(--uui-size-space-4); + } + `, + ]; +} + +export default UmbTemplateModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-template-modal': UmbTemplateModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/shared/property-action-menu/property-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/shared/property-action-menu/property-action-menu.element.ts index 2a49f5beb7..ff51de4e4d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/shared/property-action-menu/property-action-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/shared/property-action-menu/property-action-menu.element.ts @@ -26,6 +26,8 @@ export class UmbPropertyActionMenuElement extends UmbLitElement { #popover-trigger { --uui-button-padding-top-factor: 0.5; --uui-button-padding-bottom-factor: 0.1; + --uui-button-height: 18px; + --uui-button-border-radius: 6px; } #dropdown { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.context.ts new file mode 100644 index 0000000000..92d1f20549 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.context.ts @@ -0,0 +1,15 @@ +import { UmbTreeItemContextBase } from '../../../shared/components/tree/tree-item-base/tree-item-base.context'; +import { urlFriendlyPathFromServerFilePath } from '../../utils'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO get unique method from an entity repository static method +export class UmbFileSystemTreeItemContext extends UmbTreeItemContextBase { + constructor(host: UmbControllerHostInterface) { + super(host, (x: FileSystemTreeItemPresentationModel) => x.path); + } + + constructPath(pathname: string, entityType: string, path: string) { + return `section/${pathname}/workspace/${entityType}/edit/${urlFriendlyPathFromServerFilePath(path)}`; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.element.ts new file mode 100644 index 0000000000..1d18dc610a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/file-system-tree-item/file-system-tree-item.element.ts @@ -0,0 +1,49 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbFileSystemTreeItemContext } from './file-system-tree-item.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { ManifestKind } from '@umbraco-cms/backoffice/extensions-registry'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO: Move to separate file: +const manifest: ManifestKind = { + type: 'kind', + alias: 'Umb.Kind.FileSystemTreeItem', + matchKind: 'fileSystem', + matchType: 'treeItem', + manifest: { + type: 'treeItem', + elementName: 'umb-file-system-tree-item', + }, +}; +umbExtensionsRegistry.register(manifest); + +@customElement('umb-file-system-tree-item') +export class UmbFileSystemTreeItemElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + private _item?: FileSystemTreeItemPresentationModel; + @property({ type: Object, attribute: false }) + public get item() { + return this._item; + } + public set item(value: FileSystemTreeItemPresentationModel | undefined) { + this._item = value; + this.#context.setTreeItem(value); + } + + #context = new UmbFileSystemTreeItemContext(this); + + render() { + if (!this.item) return nothing; + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-file-system-tree-item': UmbFileSystemTreeItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts new file mode 100644 index 0000000000..23987fc391 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts @@ -0,0 +1 @@ +import './file-system-tree-item/file-system-tree-item.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts index a1d157077e..27ba7af32b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts @@ -1,9 +1,12 @@ import { manifests as menuManifests } from './menu.manifests'; import { manifests as templateManifests } from './templates/manifests'; +import { manifests as stylesheetManifests } from './stylesheets/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; -export const manifests = [...menuManifests, ...templateManifests]; +import './components'; + +export const manifests = [...menuManifests, ...templateManifests, ...stylesheetManifests]; const registerExtensions = (manifests: Array) => { manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/index.ts new file mode 100644 index 0000000000..7d6c362f01 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/index.ts @@ -0,0 +1,8 @@ +import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO: temp until we have a proper stylesheet model +export interface StylesheetDetails extends FileSystemTreeItemPresentationModel { + content: string; +} + +export const STYLESHEET_ENTITY_TYPE = 'stylesheet'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/manifests.ts new file mode 100644 index 0000000000..ddaa4babdc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/manifests.ts @@ -0,0 +1,6 @@ +import { manifests as repositoryManifests } from './repository/manifests'; +import { manifests as menuItemManifests } from './menu-item/manifests'; +import { manifests as treeManifests } from './tree/manifests'; +import { manifests as workspaceManifests } from './workspace/manifests'; + +export const manifests = [...repositoryManifests, ...menuItemManifests, ...treeManifests, ...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/menu-item/manifests.ts new file mode 100644 index 0000000000..c6ee826fc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/menu-item/manifests.ts @@ -0,0 +1,20 @@ +import { STYLESHEET_TREE_ALIAS } from '../tree/manifests'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const menuItem: ManifestTypes = { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Stylesheets', + name: 'Stylesheets Menu Item', + weight: 400, + meta: { + label: 'Stylesheets', + icon: 'umb:folder', + treeAlias: STYLESHEET_TREE_ALIAS, + }, + conditions: { + menus: ['Umb.Menu.Templating'], + }, +}; + +export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/manifests.ts new file mode 100644 index 0000000000..34225d54b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/manifests.ts @@ -0,0 +1,25 @@ +import { UmbStylesheetRepository } from './stylesheet.repository'; +import { UmbStylesheetTreeStore } from './stylesheet.tree.store'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestTreeStore } from '@umbraco-cms/backoffice/extensions-registry'; + +export const STYLESHEET_REPOSITORY_ALIAS = 'Umb.Repository.Stylesheet'; + +const repository: ManifestRepository = { + type: 'repository', + alias: STYLESHEET_REPOSITORY_ALIAS, + name: 'Stylesheet Repository', + class: UmbStylesheetRepository, +}; + +export const STYLESHEET_STORE_ALIAS = 'Umb.Store.Stylesheet'; +export const STYLESHEET_TREE_STORE_ALIAS = 'Umb.Store.StylesheetTree'; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: STYLESHEET_TREE_STORE_ALIAS, + name: 'Stylesheet Tree Store', + class: UmbStylesheetTreeStore, +}; + +export const manifests = [treeStore, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts new file mode 100644 index 0000000000..bc82863011 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts @@ -0,0 +1,47 @@ +import { StylesheetDetails } from '../..'; +import { DataSourceResponse, UmbDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; + +/** + * A data source for the Stylesheet that fetches data from the server + * @export + * @class UmbStylesheetServerDataSource + * @implements {UmbStylesheetServerDataSource} + */ +export class UmbStylesheetServerDataSource implements UmbDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbStylesheetServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbStylesheetServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + createScaffold(parentKey: string | null): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Fetches a Stylesheet with the given path from the server + * @param {string} path + * @return {*} + * @memberof UmbStylesheetServerDataSource + */ + async get(path: string) { + if (!path) throw new Error('Path is missing'); + console.log('GET STYLESHEET WITH PATH', path); + return { data: undefined, error: undefined }; + } + + insert(data: StylesheetDetails): Promise> { + throw new Error('Method not implemented.'); + } + update(data: StylesheetDetails): Promise> { + throw new Error('Method not implemented.'); + } + delete(key: string): Promise> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.tree.server.data.ts new file mode 100644 index 0000000000..7165e29c2e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.tree.server.data.ts @@ -0,0 +1,68 @@ +import { + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, + StylesheetResource, +} from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; + +/** + * A data source for the Stylesheet tree that fetches data from the server + * @export + * @class UmbStylesheetTreeServerDataSource + * @implements {UmbTreeDataSource} + */ +export class UmbStylesheetTreeServerDataSource + implements UmbTreeDataSource +{ + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbStylesheetTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbStylesheetTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the stylesheet tree root items from the server + * @return {*} + * @memberof UmbStylesheetTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, StylesheetResource.getTreeStylesheetRoot({})); + } + + /** + * Fetches the children of a given stylesheet path from the server + * @param {(string | undefined)} path + * @return {*} + * @memberof UmbStylesheetTreeServerDataSource + */ + async getChildrenOf(path: string | undefined) { + return tryExecuteAndNotify( + this.#host, + StylesheetResource.getTreeStylesheetChildren({ + path, + }) + ); + } + + /** + * Fetches stylesheet items from the server + * @param {(string | undefined)} path + * @return {*} + * @memberof UmbStylesheetTreeServerDataSource + */ + async getItems(path: Array) { + return tryExecuteAndNotify( + this.#host, + StylesheetResource.getTreeStylesheetItem({ + path, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.repository.ts new file mode 100644 index 0000000000..a045817b57 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.repository.ts @@ -0,0 +1,110 @@ +import { UmbStylesheetTreeStore, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN } from './stylesheet.tree.store'; +import { UmbStylesheetTreeServerDataSource } from './sources/stylesheet.tree.server.data'; +import { UmbStylesheetServerDataSource } from './sources/stylesheet.server.data'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; +import { + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, +} from '@umbraco-cms/backoffice/backend-api'; + +export class UmbStylesheetRepository + implements UmbTreeRepository +{ + #host; + #dataSource; + #treeDataSource; + #treeStore?: UmbStylesheetTreeStore; + #notificationContext?: UmbNotificationContext; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + // TODO: figure out how spin up get the correct data source + this.#dataSource = new UmbStylesheetServerDataSource(this.#host); + this.#treeDataSource = new UmbStylesheetTreeServerDataSource(this.#host); + + new UmbContextConsumerController(this.#host, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationContext = instance; + this.#checkIfInitialized(); + }); + } + + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + + #checkIfInitialized() { + if (this.#treeStore && this.#notificationContext) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async requestRootTreeItems() { + await this.#init; + + const { data, error } = await this.#treeDataSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error }; + } + + async requestTreeItemsOf(path: string | null) { + if (!path) throw new Error('Cannot request tree item with missing path'); + + await this.#init; + + const { data, error } = await this.#treeDataSource.getChildrenOf(path); + + if (data) { + this.#treeStore!.appendItems(data.items); + } + + return { data, error, asObservable: () => this.#treeStore!.childrenOf(path) }; + } + + async requestTreeItems(paths: Array) { + if (!paths) throw new Error('Paths are missing'); + await this.#init; + const { data, error } = await this.#treeDataSource.getItems(paths); + return { data, error }; + } + + async rootTreeItems() { + await this.#init; + return this.#treeStore!.rootItems; + } + + async treeItemsOf(parentPath: string | null) { + if (!parentPath) throw new Error('Parent Path is missing'); + await this.#init; + return this.#treeStore!.childrenOf(parentPath); + } + + async treeItems(paths: Array) { + if (!paths) throw new Error('Paths are missing'); + await this.#init; + return this.#treeStore!.items(paths); + } + + // DETAILS + async requestByPath(path: string) { + if (!path) throw new Error('Path is missing'); + await this.#init; + const { data, error } = await this.#dataSource.get(path); + return { data, error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.tree.store.ts new file mode 100644 index 0000000000..bf7a427c5c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/stylesheet.tree.store.ts @@ -0,0 +1,24 @@ +import { UmbFileSystemTreeStore } from '@umbraco-cms/backoffice/store'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; + +/** + * @export + * @class UmbStylesheetTreeStore + * @extends {UmbEntityTreeStore} + * @description - Tree Data Store for Stylesheets + */ +export class UmbStylesheetTreeStore extends UmbFileSystemTreeStore { + /** + * Creates an instance of UmbStylesheetTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbStylesheetTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN.toString()); + } +} + +export const UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + 'UmbStylesheetTreeStore' +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/tree/manifests.ts new file mode 100644 index 0000000000..c93536bd70 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/tree/manifests.ts @@ -0,0 +1,27 @@ +import { STYLESHEET_ENTITY_TYPE } from '..'; +import { STYLESHEET_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; + +export const STYLESHEET_TREE_ALIAS = 'Umb.Tree.Stylesheet'; + +const tree: ManifestTree = { + type: 'tree', + alias: STYLESHEET_TREE_ALIAS, + name: 'Stylesheet Tree', + weight: 10, + meta: { + repositoryAlias: STYLESHEET_REPOSITORY_ALIAS, + }, +}; + +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'fileSystem', + alias: 'Umb.TreeItem.Stylesheet', + name: 'Stylesheet Tree Item', + conditions: { + entityType: STYLESHEET_ENTITY_TYPE, + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/manifests.ts new file mode 100644 index 0000000000..ad0675cf06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/manifests.ts @@ -0,0 +1,20 @@ +import type { + ManifestWorkspace, + ManifestWorkspaceAction, + ManifestWorkspaceView, +} from '@umbraco-cms/backoffice/extensions-registry'; + +const workspace: ManifestWorkspace = { + type: 'workspace', + alias: 'Umb.Workspace.StyleSheet', + name: 'Stylesheet Workspace', + loader: () => import('./stylesheet-workspace.element'), + meta: { + entityType: 'stylesheet', + }, +}; + +const workspaceViews: Array = []; +const workspaceActions: Array = []; + +export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts new file mode 100644 index 0000000000..69c89804e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts @@ -0,0 +1,29 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +@customElement('umb-stylesheet-workspace-edit') +export class UmbStylesheetWorkspaceEditElement extends LitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + `, + ]; + + render() { + return html` Stylesheet workspace `; + } +} + +export default UmbStylesheetWorkspaceEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-stylesheet-workspace-edit': UmbStylesheetWorkspaceEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts new file mode 100644 index 0000000000..e3be06cf52 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -0,0 +1,42 @@ +import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; +import { UmbStylesheetRepository } from '../repository/stylesheet.repository'; +import { StylesheetDetails } from '..'; +import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; +import { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbStylesheetWorkspaceContext + extends UmbWorkspaceContext + implements UmbWorkspaceContextInterface +{ + #data = new ObjectState(undefined); + data = this.#data.asObservable(); + + constructor(host: UmbControllerHostInterface) { + super(host, new UmbStylesheetRepository(host)); + } + + getEntityType(): string { + return 'stylesheet'; + } + + getData() { + return this.#data.getValue(); + } + + getEntityKey() { + return this.getData()?.path || ''; + } + + async load(path: string) { + const { data } = await this.repository.requestByPath(path); + if (data) { + this.setIsNew(false); + this.#data.update(data); + } + } + + public destroy(): void { + this.#data.complete(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.element.ts new file mode 100644 index 0000000000..a0e2ba7e85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.element.ts @@ -0,0 +1,50 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { IRoutingInfo } from 'router-slot'; +import { serverFilePathFromUrlFriendlyPath } from '../../utils'; +import { UmbStylesheetWorkspaceEditElement } from './stylesheet-workspace-edit.element'; +import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +@customElement('umb-stylesheet-workspace') +export class UmbStylesheetWorkspaceElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + `, + ]; + + #workspaceContext = new UmbStylesheetWorkspaceContext(this); + #element = new UmbStylesheetWorkspaceEditElement(); + + @state() + _routes: any[] = [ + { + path: 'edit/:path', + component: () => this.#element, + setup: (component: HTMLElement, info: IRoutingInfo) => { + const path = info.match.params.path; + const serverPath = serverFilePathFromUrlFriendlyPath(path); + this.#workspaceContext.load(serverPath); + }, + }, + ]; + + render() { + return html` `; + } +} + +export default UmbStylesheetWorkspaceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-stylesheet-workspace': UmbStylesheetWorkspaceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.stories.ts new file mode 100644 index 0000000000..9642c1b6bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.stories.ts @@ -0,0 +1,16 @@ +import './stylesheet-workspace.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit'; + +import type { UmbStylesheetWorkspaceElement } from './stylesheet-workspace.element'; + +export default { + title: 'Workspaces/Stylesheet', + component: 'umb-stylesheet-workspace', + id: 'umb-stylesheet-workspace', +} as Meta; + +export const AAAOverview: Story = () => + html` `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts index f7bdb4dd69..ec47ab953d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts @@ -8,7 +8,7 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { ProblemDetailsModel, TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; -export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailRepository { #init; #host: UmbControllerHostInterface; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts index ecbe8c9031..b606566c98 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts @@ -1,10 +1,8 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -export const UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbTemplateTreeStore' -); +export const UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbTemplateTreeStore'); /** * @export @@ -12,7 +10,7 @@ export const UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken encodeURIComponent(path).replace('.', '-'); + +// TODO: we can try and make pretty urls if we want to +export const serverFilePathFromUrlFriendlyPath = (unique: string) => decodeURIComponent(unique.replace('-', '.')); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts index 59075cfc5a..ac680847b3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts @@ -5,7 +5,7 @@ import { UmbDictionaryTreeStore, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from import { DictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { RepositoryTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; @@ -14,7 +14,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo #host: UmbControllerHostInterface; - #treeSource: RepositoryTreeDataSource; + #treeSource: UmbTreeDataSource; #treeStore?: UmbDictionaryTreeStore; #detailSource: UmbDictionaryDetailServerDataSource; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts index 8c3575e356..cfcbb6ea48 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts @@ -1,14 +1,14 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbDictionaryTreeStore - * @extends {UmbTreeStoreBase} + * @extends {UmbEntityTreeStore} * @description - Tree Data Store for Dictionary */ -export class UmbDictionaryTreeStore extends UmbTreeStoreBase { +export class UmbDictionaryTreeStore extends UmbEntityTreeStore { /** * Creates an instance of UmbDictionaryTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts index 55ac44213f..a96b16666d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts @@ -1,6 +1,6 @@ import { DictionaryResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller'; -import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** @@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class DictionaryTreeServerDataSource * @implements {DictionaryTreeDataSource} */ -export class DictionaryTreeServerDataSource implements RepositoryTreeDataSource { +export class DictionaryTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostInterface; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts index 50c7f506a6..dcfeba9411 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts @@ -1,13 +1,23 @@ -import { UmbDictionaryRepository } from '../repository/dictionary.repository'; -import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; +import { DICTIONARY_REPOSITORY_ALIAS } from '../repository/manifests'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.Dictionary', name: 'Dictionary Tree', meta: { - repository: UmbDictionaryRepository, + repositoryAlias: DICTIONARY_REPOSITORY_ALIAS, }, }; -export const manifests = [tree]; +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'entity', + alias: 'Umb.TreeItem.DictionaryItem', + name: 'Dictionary Item Tree Item', + conditions: { + entityType: 'dictionary-item', + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index 1219928040..a756d4c727 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -28,6 +28,7 @@ import { handlers as redirectManagementHandlers } from './domains/redirect-manag import { handlers as logViewerHandlers } from './domains/log-viewer.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; import { handlers as rteEmbedHandlers } from './domains/rte-embed.handlers'; +import { handlers as stylesheetHandlers } from './domains/stylesheet.handlers'; const handlers = [ serverHandlers.serverVersionHandler, @@ -59,6 +60,7 @@ const handlers = [ ...logViewerHandlers, ...packageHandlers, ...rteEmbedHandlers, + ...stylesheetHandlers, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts index 4e847d3eff..ec6d416519 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts @@ -890,8 +890,12 @@ export const data: Array = [ }, }, { - allowedTemplateKeys: [], - defaultTemplateKey: null, + allowedTemplateKeys: [ + '2bf464b6-3aca-4388-b043-4eb439cc2643', + '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', + '9a84c0b3-03b4-4dd4-84ac-706740ac0f72', + ], + defaultTemplateKey: '2bf464b6-3aca-4388-b043-4eb439cc2643', key: 'simple-document-type-key', alias: 'simpleDocumentType', name: 'Simple Document Type', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts index 811f630616..e1fa80708c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts @@ -543,7 +543,7 @@ export const treeData: Array = [ name: 'All property editors', type: 'document', icon: 'document', - hasChildren: true, + hasChildren: false, }, { $type: 'DocumentTreeItemViewModel', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/stylesheet.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/stylesheet.data.ts new file mode 100644 index 0000000000..f73ee09968 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/stylesheet.data.ts @@ -0,0 +1,80 @@ +import { UmbEntityData } from './entity.data'; +import { createFileSystemTreeItem } from './utils'; +import { + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, +} from '@umbraco-cms/backoffice/backend-api'; + +type StylesheetDBItem = FileSystemTreeItemPresentationModel & { + content: string; +}; + +export const data: Array = [ + { + path: 'Stylesheet File 1.css', + isFolder: false, + name: 'Stylesheet File 1.css', + type: 'stylesheet', + icon: 'umb:brackets', + hasChildren: false, + content: `Stylesheet content 1`, + }, + { + path: 'Stylesheet File 2.css', + isFolder: false, + name: 'Stylesheet File 2.css', + type: 'stylesheet', + icon: 'umb:brackets', + hasChildren: false, + content: `Stylesheet content 2`, + }, + { + path: 'Folder 1', + isFolder: true, + name: 'Folder 1', + type: 'stylesheet', + icon: 'umb:folder', + hasChildren: true, + content: `Stylesheet content 3`, + }, + { + path: 'Folder 1/Stylesheet File 3.css', + isFolder: false, + name: 'Stylesheet File 3.css', + type: 'stylesheet', + icon: 'umb:brackets', + hasChildren: false, + content: `Stylesheet content 3`, + }, +]; + +// Temp mocked database +// TODO: all properties are optional in the server schema. I don't think this is correct. +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +class UmbStylesheetData extends UmbEntityData { + constructor() { + super(data); + } + + getTreeRoot(): PagedFileSystemTreeItemPresentationModel { + const items = this.data.filter((item) => item.path?.includes('/') === false); + const treeItems = items.map((item) => createFileSystemTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItemChildren(parentPath: string): PagedFileSystemTreeItemPresentationModel { + const items = this.data.filter((item) => item.path?.startsWith(parentPath + '/')); + const treeItems = items.map((item) => createFileSystemTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItem(paths: Array): Array { + const items = this.data.filter((item) => paths.includes(item.path ?? '')); + return items.map((item) => createFileSystemTreeItem(item)); + } +} + +export const umbStylesheetData = new UmbStylesheetData(); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts index 257b43976e..325e8e46b0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts @@ -6,6 +6,7 @@ import type { FolderTreeItemResponseModel, DocumentTypeResponseModel, DocumentResponseModel, + FileSystemTreeItemPresentationModel, } from '@umbraco-cms/backoffice/backend-api'; export const createEntityTreeItem = (item: any): EntityTreeItemResponseModel => { @@ -38,7 +39,9 @@ export const createContentTreeItem = (item: any): ContentTreeItemResponseModel & }; // TODO: remove isTrashed type extension when we have found a solution to trashed items -export const createDocumentTreeItem = (item: DocumentResponseModel): DocumentTreeItemResponseModel & { isTrashed: boolean } => { +export const createDocumentTreeItem = ( + item: DocumentResponseModel +): DocumentTreeItemResponseModel & { isTrashed: boolean } => { return { ...createContentTreeItem(item), /* @@ -57,3 +60,10 @@ export const createDocumentTypeTreeItem = (item: DocumentTypeResponseModel): Doc isElement: item.isElement, }; }; + +export const createFileSystemTreeItem = (item: any): FileSystemTreeItemPresentationModel => { + return { + ...createFolderTreeItem(item), + path: item.path, + }; +}; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/rte-embed.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/rte-embed.handlers.ts index dd811943f0..f08e233ed5 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/rte-embed.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/rte-embed.handlers.ts @@ -1,5 +1,5 @@ import { rest } from 'msw'; -import { OEmbedResult, OEmbedStatus } from '@umbraco-cms/backoffice/modal'; +import { OEmbedResult, OEmbedStatus } from '../../../backoffice/shared/modals/embedded-media'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/stylesheet.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/stylesheet.handlers.ts new file mode 100644 index 0000000000..ef252a199e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/stylesheet.handlers.ts @@ -0,0 +1,26 @@ +import { rest } from 'msw'; +import { umbStylesheetData } from '../data/stylesheet.data'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const handlers = [ + rest.get(umbracoPath('/tree/stylesheet/root'), (req, res, ctx) => { + const response = umbStylesheetData.getTreeRoot(); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/stylesheet/children'), (req, res, ctx) => { + const path = req.url.searchParams.get('path'); + if (!path) return; + + const response = umbStylesheetData.getTreeItemChildren(path); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/stylesheet/item'), (req, res, ctx) => { + const paths = req.url.searchParams.getAll('paths'); + if (!paths) return; + + const items = umbStylesheetData.getTreeItem(paths); + return res(ctx.status(200), ctx.json(items)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx index c0ede6a70b..dbb9dcb584 100644 --- a/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx @@ -16,3 +16,4 @@ The Umbraco Backoffice currently support the following extension types: - [Property Editor](?path=/docs/guides-extending-the-backoffice-property-editors--docs) - [Repository](?path=/docs/guides-extending-the-backoffice-property-editors--docs) - [Menu](?path=/docs/guides-extending-the-backoffice-menu--docs) +- [Tree](?path=/docs/guides-extending-the-backoffice-trees--docs) diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/tree.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/tree.mdx new file mode 100644 index 0000000000..98b44058f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/tree.mdx @@ -0,0 +1,136 @@ +import { Meta } from '@storybook/addon-docs'; + + + +# Trees + +// TODO: add description of trees. +Rough notes: +The tree is a hierarchical structure of nodes. The tree is registered in the Backoffice extension registry. A tree can be rendered anywhere in the Backoffice with the help of the umb-tree element. + +## Registering a tree + +Tree Manifest + +```json +// TODO: add interface +{ + "type": "tree", + "alias": "My.Tree.Alias", + "name": "My Tree", + "meta": { + "repositoryAlias": "My.Repository.Alias" + } +}, +{ + "type": "treeItem", + "kind": "entity", + "alias": "My.TreeItem.Alias", + "name": "My Tree Item", + "conditions": { + "entityType": "my-entity-type", + }, +} +``` + +The backoffice comes with two different tree item kinds out of the box: +entity and fileSystem + +## Rendering a tree + +```html + +``` + +## Render a Custom Tree Item + +The Tree Item Manifest + +```json +{ + "type": "treeItem", + "alias": "Umb.TreeItem.Alias", + "name": "My Tree Item", + "js": "./my-tree-item.element.js", + "conditions": { + "entityType": "my-entity-type", + }, +}; +``` + +### The Tree Item Element + +```typescript +import { css, html, nothing } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element'; +import { UmbMyTreeItemContext, MyTreeItemDataModel } from './my-tree-item.context'; + +@customElement('my-tree-item') +export class MyTreeItemElement extends UmbElementMixin(LitElement) { + private _item?: MyTreeItemDataModel; + @property({ type: Object, attribute: false }) + public get item() { + return this._item; + } + public set item(value: MyTreeItemDataModel | undefined) { + this._item = value; + this.#context.setTreeItem(value); + } + + #context = new UmbMyTreeItemContext(this); + + render() { + if (!this.item) return nothing; + return html` Some custom markup `; + } +} + +export default MyTreeItemElement; +``` + +### The Tree Item Context + +```typescript +// TODO: auto generate this from the interface +export interface UmbTreeItemContext { + host: UmbControllerHostInterface; + unique?: string; + type?: string; + + treeItem: Observable; + hasChildren: Observable; + isLoading: Observable; + isSelectable: Observable; + isSelected: Observable; + isActive: Observable; + hasActions: Observable; + path: Observable; + + setTreeItem(treeItem: T | undefined): void; + + requestChildren(): Promise<{ + data: PagedResponse | undefined; + error: ProblemDetailsModel | undefined; + asObservable?: () => Observable; + }>; + toggleContextMenu(): void; + select(): void; + deselect(): void; + constructPath(pathname: string, entityType: string, unique: string): string; +} +``` + +### Extending the Tree Item Context base + +We provide a base class for the tree item context. This class provides some default implementations for the context. You can extend this class to overwrite any of the default implementations. + +```typescript +export class UmbMyTreeItemContext extends UmbTreeItemContextBase { + constructor(host: UmbControllerHostInterface) { + super(host, (x: MyTreeItemDataModel) => x.unique); + } + + // overwrite any methods or properties here if needed +} +```