diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section-tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section-tree.element.ts index f5b2bfd084..caa34cc1f1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section-tree.element.ts @@ -5,8 +5,11 @@ import { data as dataTypeData } from '../../../mocks/data/data-type.data'; import { data as documentTypeData } from '../../../mocks/data/document-type.data'; import { UmbContextConsumerMixin } from '../../../core/context'; import { UmbDataTypeStore } from '../../../core/stores/data-type.store'; -import { Subscription } from 'rxjs'; +import { map, Subscription, first } from 'rxjs'; import { UmbDocumentTypeStore } from '../../../core/stores/document-type.store'; +import { createExtensionElement, UmbExtensionRegistry } from '../../../core/extension'; +import '../../tree/tree.element'; +import { UmbSectionContext } from '../section.context'; @customElement('umb-settings-section-tree') class UmbSettingsSectionTree extends UmbContextConsumerMixin(LitElement) { @@ -26,15 +29,32 @@ class UmbSettingsSectionTree extends UmbContextConsumerMixin(LitElement) { @state() _documentTypes: Array = []; + @state() + private _trees: Array = []; + + @state() + private _currentSectionAlias?: string; + private _dataTypeStore?: UmbDataTypeStore; private _documentTypeStore?: UmbDocumentTypeStore; private _dataTypesSubscription?: Subscription; private _documentTypesSubscription?: Subscription; + private _extensionStore?: UmbExtensionRegistry; + private _treeSubscription?: Subscription; + + private _sectionContextSubscription?: Subscription; + private _sectionContext?: UmbSectionContext; + constructor() { super(); + this.consumeContext('umbSectionContext', (context: UmbSectionContext) => { + this._sectionContext = context; + this._useSectionContext(); + }); + // TODO: temp solution until we know where to get tree data from this.consumeContext('umbDataTypeStore', (store) => { this._dataTypeStore = store; @@ -52,6 +72,11 @@ class UmbSettingsSectionTree extends UmbContextConsumerMixin(LitElement) { this._documentTypes = documentTypes; }); }); + + this.consumeContext('umbExtensionRegistry', (store) => { + this._extensionStore = store; + this._useTrees(); + }); } disconnectedCallback(): void { @@ -61,6 +86,35 @@ class UmbSettingsSectionTree extends UmbContextConsumerMixin(LitElement) { this._documentTypesSubscription?.unsubscribe(); } + private _useSectionContext() { + this._sectionContextSubscription?.unsubscribe(); + + this._sectionContextSubscription = this._sectionContext?.data.pipe(first()).subscribe((section) => { + this._currentSectionAlias = section.alias; + }); + } + + private _useTrees() { + //TODO: Merge streams + if (this._extensionStore && this._currentSectionAlias) { + this._treeSubscription?.unsubscribe(); + + this._treeSubscription = this._extensionStore + ?.extensionsOfType('tree') + .pipe( + map((extensions) => + extensions + .filter((extension) => extension.meta.sections.includes(this._currentSectionAlias as string)) // TODO: Why do whe need "as string" here?? + .sort((a, b) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((treeExtensions) => { + this._trees = treeExtensions; + console.log('Wrosk', this._trees); + }); + } + } + render() { return html` @@ -85,6 +139,8 @@ class UmbSettingsSectionTree extends UmbContextConsumerMixin(LitElement) { ` )} + + ${this._trees.map((tree) => html``)} `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tree/datatypes-tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tree/datatypes-tree.element.ts new file mode 100644 index 0000000000..6b8a179c6f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tree/datatypes-tree.element.ts @@ -0,0 +1,20 @@ +import { css, html, LitElement } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement } from 'lit/decorators.js'; +import './tree-navigator.element'; +import './tree-item.element'; + +@customElement('umb-datatype-tree') +export class UmbDatatypeTree extends LitElement { + static styles = [UUITextStyles, css``]; + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-datatype-tree': UmbDatatypeTree; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tree/document-type-tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tree/document-type-tree.element.ts new file mode 100644 index 0000000000..0e37af34a2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tree/document-type-tree.element.ts @@ -0,0 +1,23 @@ +import { css, html, LitElement } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import './tree-navigator.element'; +import './tree-item.element'; + +@customElement('umb-document-type-tree') +export class UmbDocumentTypeTree extends LitElement { + static styles = [UUITextStyles, css``]; + + @property({ type: String }) + public alias = ''; + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-type-tree': UmbDocumentTypeTree; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree-navigator.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree-navigator.element.ts index 270d248b6c..1aaddd9a78 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree-navigator.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree-navigator.element.ts @@ -10,19 +10,35 @@ export class UmbTreeNavigator extends UmbContextProviderMixin(LitElement) { private _treeService: UmbTreeService; + @state() + id = '2'; + + @state() + label = ''; + + @state() + hasChildren = false; + + @state() + loading = true; + constructor() { super(); this._treeService = new UmbTreeService(); this.provideContext('umbTreeService', this._treeService); - } - - renderItems() { - return this._treeService.getRoot().map((item) => { - return html``; + this._treeService.getTreeItem(this.id).then((item) => { + this.label = item.name; + this.hasChildren = item.hasChildren; + this.loading = false; }); } + render() { - return this.renderItems(); + return html` `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.element.ts new file mode 100644 index 0000000000..a6ee776437 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.element.ts @@ -0,0 +1,46 @@ +import { css, CSSResultGroup, html, LitElement } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { createExtensionElement, UmbExtensionManifestTree } from '../../core/extension'; +import { UmbTreeNavigator } from './tree-navigator.element'; + +@customElement('umb-tree') +export class UmbTree extends LitElement { + static styles: CSSResultGroup = [UUITextStyles]; + + private _tree?: UmbExtensionManifestTree; + + @property({ type: Object }) + public get tree(): UmbExtensionManifestTree | undefined { + return this._tree; + } + public set tree(value: UmbExtensionManifestTree | undefined) { + this._tree = value; + this._createElement(); + } + + @state() + private _element?: any; + + private async _createElement() { + if (!this.tree) return; + + try { + this._element = (await createExtensionElement(this.tree)) as any | undefined; + if (!this._element) return; + this._element.alias = this._tree?.alias; + } catch (error) { + // TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.) + } + } + + render() { + return html`${this._element}`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree': UmbTree; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.service.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.service.ts index e954463d41..e90ef8af89 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.service.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tree/tree.service.ts @@ -5,13 +5,18 @@ export class UmbTreeService { return fakeApi.getTreeRoot(); } + public async getTreeItem(id: string) { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 2000)); + return fakeApi.getTreeItem(id); + } + public async getChildren(id: string) { await new Promise((resolve) => setTimeout(resolve, Math.random() * 2000)); return fakeApi.getTreeChildren(id); } } -// EVERYTHING BELOW IS FAKE DATA +// EVERYTHING BELOW IS FAKE MOCK DATA AND WILL BE REMOVED const fakeApi = { //find nested child id of array @@ -28,6 +33,16 @@ const fakeApi = { }); }, + getTreeItem: (id: string) => { + const item = recursive(treeData, id); + if (!item) return 'not found'; + + return { + ...item, + hasChildren: item.children.length > 0, + }; + }, + getTreeRoot: () => { return treeData.map((item) => { return { @@ -79,20 +94,20 @@ const treeData = [ }, { id: '2', - name: 'Templates', + name: 'DataTypes', children: [ { id: '2-1', - name: 'Templates-2-1', + name: 'DataTypes-2-1', children: [], }, { id: '2-2', - name: 'Templates-2-2', + name: 'DataTypes-2-2', children: [ { id: '2-2-1', - name: 'Templates-2-2-1', + name: 'DataTypes-2-2-1', children: [], }, ], diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts index 815853d89f..545c86fa2d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts @@ -25,6 +25,16 @@ export type UmbExtensionManifestSection = { meta: UmbManifestSectionMeta; } & UmbExtensionManifestBase; +//tree +export type UmbManifestTreeMeta = { + weight: number; + sections: Array; +}; +export type UmbExtensionManifestTree = { + type: 'tree'; + meta: UmbManifestTreeMeta; +} & UmbExtensionManifestBase; + // propertyEditor: export type UmbManifestPropertyEditorMeta = { icon: string; @@ -74,6 +84,7 @@ export type UmbExtensionManifestEditorView = { export type UmbExtensionManifestCore = | UmbExtensionManifestSection + | UmbExtensionManifestTree | UmbExtensionManifestDashboard | UmbExtensionManifestPropertyEditorUI | UmbExtensionManifestPropertyAction @@ -115,6 +126,7 @@ export class UmbExtensionRegistry { // Typings concept, need to put all core types to get a good array return type for the provided type... extensionsOfType(type: 'section'): Observable>; + extensionsOfType(type: 'tree'): Observable>; extensionsOfType(type: 'dashboard'): Observable>; extensionsOfType(type: 'editorView'): Observable>; extensionsOfType(type: 'propertyEditorUI'): Observable>; diff --git a/src/Umbraco.Web.UI.Client/src/core/stores/entity.store.ts b/src/Umbraco.Web.UI.Client/src/core/stores/entity.store.ts new file mode 100644 index 0000000000..7288941cf0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/stores/entity.store.ts @@ -0,0 +1,38 @@ +import { BehaviorSubject, map, Observable } from 'rxjs'; +import { Entity } from '../../mocks/data/entity.data'; +import { umbNodeData } from '../../mocks/data/node.data'; + +export class UmbEntityStore { + private _entities: BehaviorSubject> = new BehaviorSubject(>[]); + public readonly entities: Observable> = this._entities.asObservable(); + + getById(id: number): Observable { + // fetch from server and update store + fetch(`/umbraco/backoffice/content/${id}`) + .then((res) => res.json()) + .then((data) => { + this._updateStore(data); + }); + + return this.entities.pipe(map((nodes: Array) => nodes.find((node: Entity) => node.id === id) || null)); + } + + private _updateStore(fetchedNodes: Array) { + const storedNodes = this._entities.getValue(); + const updated: Entity[] = [...storedNodes]; + + fetchedNodes.forEach((fetchedNode) => { + const index = storedNodes.map((storedNode) => storedNode.id).indexOf(fetchedNode.id); + + if (index !== -1) { + // If the node is already in the store, update it + updated[index] = fetchedNode; + } else { + // If the node is not in the store, add it + updated.push(fetchedNode); + } + }); + + this._entities.next([...updated]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts new file mode 100644 index 0000000000..6de052818a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts @@ -0,0 +1,43 @@ +export interface Entity { + id: number; + key: string; + name: string; + icon: string; // TODO: Should this be here? + type: string; + hasChildren: boolean; // TODO: Should this be here? +} + +export const data: Array = [ + { + id: 1, + key: '74e4008a-ea4f-4793-b924-15e02fd380d1', + name: 'Document 1', + type: 'document', + icon: 'document', + hasChildren: false, + }, + { + id: 2, + key: '74e4008a-ea4f-4793-b924-15e02fd380d2', + name: 'Document 2', + type: 'document', + icon: 'favorite', + hasChildren: false, + }, + { + id: 3, + key: 'cdd30288-2d1c-41b4-89a9-61647b4a10d5', + name: 'Document 3', + type: 'document', + icon: 'document', + hasChildren: false, + }, + { + id: 2001, + key: 'f2f81a40-c989-4b6b-84e2-057cecd3adc1', + name: 'Media 1', + type: 'media', + icon: 'picture', + hasChildren: false, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/domains/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/domains/tree.handlers.ts new file mode 100644 index 0000000000..4e8efc78b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/domains/tree.handlers.ts @@ -0,0 +1 @@ +//TODO: MAKE diff --git a/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts b/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts index d86cfbb8af..76c36ff67c 100644 --- a/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/temp-internal-manifests.ts @@ -241,4 +241,37 @@ export const internalManifests: Array = [ group: 'common', }, }, + { + type: 'tree', + alias: 'Umb.Tree.Datatypes', + name: 'DataTypes', + elementName: 'umb-datatype-tree', + js: () => import('./backoffice/tree/datatypes-tree.element'), + meta: { + weight: -10, + sections: ['Umb.Section.Settings'], + }, + }, + { + type: 'tree', + alias: 'Umb.Tree.DocumentTypes', + name: 'DocumentTypes', + elementName: 'umb-document-type-tree', + js: () => import('./backoffice/tree/document-type-tree.element'), + meta: { + weight: -10, + sections: ['Umb.Section.Settings'], + }, + }, + { + type: 'tree', + alias: 'Umb.Tree.DocumentTypes', + name: 'DocumentTypes', + elementName: 'umb-document-type-tree', + js: () => import('./backoffice/tree/document-type-tree.element'), + meta: { + weight: -10, + sections: ['Umb.Section.Content'], + }, + }, ];