From e7abf9e2fceecf20222a20379faa46e7b77478ae Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Fri, 27 Jan 2023 14:53:37 +1000 Subject: [PATCH 1/9] add member group tree store --- .../src/backoffice/backoffice.element.ts | 6 +- ....store.ts => member-group.detail.store.ts} | 10 +- .../member-groups/member-group.tree.store.ts | 96 +++++++++++++++++++ .../members/member-groups/tree/manifests.ts | 5 +- .../src/core/mocks/data/member-group.data.ts | 4 +- 5 files changed, 110 insertions(+), 11 deletions(-) rename src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/{member-group.details.store.ts => member-group.detail.store.ts} (67%) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 1d684f6192..c60a13f393 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -23,7 +23,8 @@ import { UmbMediaDetailStore } from './media/media/media.detail.store'; import { UmbMediaTreeStore } from './media/media/media.tree.store'; import { UmbMemberTypeDetailStore } from './members/member-types/member-type.detail.store'; import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store'; -import { UmbMemberGroupStore } from './members/member-groups/member-group.details.store'; +import { UmbMemberGroupDetailStore } from './members/member-groups/member-group.detail.store'; +import { UmbMemberGroupTreeStore } from './members/member-groups/member-group.tree.store'; import { UmbDictionaryDetailStore } from './translation/dictionary/dictionary.detail.store'; import { UmbDictionaryTreeStore } from './translation/dictionary/dictionary.tree.store'; import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store'; @@ -86,7 +87,8 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbMemberTypeDetailStore(this); new UmbMemberTypeTreeStore(this); new UmbUserGroupStore(this); - new UmbMemberGroupStore(this); + new UmbMemberGroupDetailStore(this); + new UmbMemberGroupTreeStore(this); new UmbDictionaryDetailStore(this); new UmbDictionaryTreeStore(this); new UmbDocumentBlueprintDetailStore(this); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts similarity index 67% rename from src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts index 8f2f06aec1..bee1d597e8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -5,15 +5,15 @@ import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbStoreBase } from '@umbraco-cms/store'; -export const UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupStore'); +export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupDetailStore'); /** * @export - * @class UmbMemberGroupStore + * @class UmbMemberGroupDetailStore * @extends {UmbStoreBase} - * @description - Data Store for Member Groups + * @description - Detail Data Store for Member Groups */ -export class UmbMemberGroupStore extends UmbStoreBase { +export class UmbMemberGroupDetailStore extends UmbStoreBase { #groups = new ArrayState([], x => x.key); @@ -21,7 +21,7 @@ export class UmbMemberGroupStore extends UmbStoreBase { constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString()); } getByKey(key: string): Observable { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts new file mode 100644 index 0000000000..19807891a1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts @@ -0,0 +1,96 @@ +import { EntityTreeItem, MemberGroupResource, } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupTreeStore'); + + +/** + * @export + * @class UmbMemberGroupTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Member Groups + */ +export class UmbMemberGroupTreeStore extends UmbStoreBase { + + + // TODO: use the right type here: + #data = new ArrayState([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbMemberGroupsStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/member-group/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, MemberGroupResource.getTreeMemberGroupRoot({})).then(({ data }) => { + if (data) { + // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + this.#data.append(data.items); + } + }); + + // TODO: remove ignore when we know how to handle trashed items. + return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null)); + } + + getTreeItemChildren(key: string) { + /* + tryExecuteAndNotify( + this._host, + MemberTypeResource.getTreeMemberTypeChildren({ + parentKey: key, + }) + ).then(({ data }) => { + if (data) { + // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + this.#data.append(data.items); + } + }); + */ + + return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key)); + } + + getTreeItems(keys: Array) { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + MemberGroupResource.getTreeMemberGroupItem({ + key: keys, + }) + ).then(({ data }) => { + if (data) { + // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + this.#data.append(data); + } + }); + } + + return createObservablePart(this.#data, (items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} 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 8f65257d9a..0af5f03c98 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 { UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN } from '../member-group.details.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; +import { UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../member-group.tree.store'; const treeAlias = 'Umb.Tree.MemberGroups'; @@ -7,8 +7,9 @@ const tree: ManifestTree = { type: 'tree', alias: treeAlias, name: 'Member Groups Tree', + weight: 100, meta: { - storeAlias: UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString(), + storeAlias: UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts index 907ce96d68..19f363423d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts @@ -1,7 +1,7 @@ +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; import { UmbEntityData } from './entity.data'; import { createEntityTreeItem } from './utils'; -import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; -import type { MemberGroupDetails } from '@umbraco-cms/models'; export const data: Array = [ { From 9a3e147287c43e8cb7eabb7113c3cafefc06e484 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Fri, 27 Jan 2023 16:12:38 +1000 Subject: [PATCH 2/9] add member group edit view add context etc required for initialising view --- .../member-group.detail.store.ts | 32 ++++++- .../action-member-group-delete.element.ts | 57 ++++++++++++ .../members/member-groups/tree/manifests.ts | 15 ++- .../member-groups/workspace/manifests.ts | 31 ++++++- .../member-group-workspace.context.ts | 27 ++++++ .../member-group-workspace.element.ts | 66 ++++++++++++- .../member-group-workspace.stories.ts | 18 ++++ ...orkspace-view-member-group-info.element.ts | 92 +++++++++++++++++++ ...orkspace-view-member-group-info.stories.ts | 27 ++++++ .../workspace-property-layout.element.ts | 14 ++- 10 files changed, 365 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts index bee1d597e8..de84905958 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -1,9 +1,12 @@ import { Observable } from 'rxjs'; import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; +import { ArrayState, createObservablePart } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbStoreBase } from '@umbraco-cms/store'; +import { DictionaryItem, MemberGroupResource } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { umbMemberGroupData } from 'src/core/mocks/data/member-group.data'; export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupDetailStore'); @@ -20,12 +23,33 @@ export class UmbMemberGroupDetailStore extends UmbStoreBase { public groups = this.#groups.asObservable(); - constructor(host: UmbControllerHostInterface) { + constructor(private host: UmbControllerHostInterface) { super(host, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString()); } - getByKey(key: string): Observable { - return null as any; + /** + * @description - Request a Member Group by key. The Member Group is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbMemberGroupDetailStore + */ + getByKey(key: string): Observable { + // tryExecuteAndNotify(this.host, MemberGroupResource.getMemberGroupByKey({ key })).then(({ data }) => { + // if (data) { + // this.#groups.appendOne(data); + // } + // }); + + // temp until Resource is updated + const group = umbMemberGroupData.getByKey(key); + if (group) { + this.#groups.appendOne(group); + } + + return createObservablePart( + this.#groups, + (groups) => groups.find((group) => group.key === key) as MemberGroupDetails + ); } async save(mediaTypes: Array): Promise { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts new file mode 100644 index 0000000000..231fda58ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts @@ -0,0 +1,57 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; +import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; +import { + UmbMemberGroupDetailStore, + UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN, +} from '../../member-group.detail.store'; + +@customElement('umb-tree-action-member-group-delete') +export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemActionElement { + static styles = [UUITextStyles, css``]; + + private _modalService?: UmbModalService; + private _memberGroupDetailStore?: UmbMemberGroupDetailStore; + + connectedCallback(): void { + super.connectedCallback(); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (modalService) => { + this._modalService = modalService; + }); + + this.consumeContext(UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN, (memberGroupDetailStore) => { + this._memberGroupDetailStore = memberGroupDetailStore; + }); + } + + private _handleLabelClick() { + const modalHandler = this._modalService?.confirm({ + headline: `Delete ${this._activeTreeItem?.name ?? 'item'}`, + content: 'Are you sure you want to delete this item?', + color: 'danger', + confirmLabel: 'Delete', + }); + + modalHandler?.onClose().then(({ confirmed }: any) => { + if (confirmed && this._treeContextMenuService && this._memberGroupDetailStore && this._activeTreeItem) { + this._memberGroupDetailStore?.trash([this._activeTreeItem.key]); + this._treeContextMenuService.close(); + } + }); + } + + render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree-action-member-group-delete': UmbTreeActionMemberGroupDeleteElement; + } +} 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 0af5f03c98..ede9dff6de 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 @@ -13,6 +13,19 @@ const tree: ManifestTree = { }, }; -const treeItemActions: Array = []; +const treeItemActions: Array = [ + { + type: 'treeItemAction', + alias: 'Umb.TreeItemAction.MemberGroup.Delete', + name: 'Member Group Tree Item Action Delete', + loader: () => import('./actions/action-member-group-delete.element'), + weight: 100, + meta: { + entityType: 'member-group', + label: 'Delete', + icon: 'delete', + }, + }, +]; export const manifests = [tree, ...treeItemActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts index abea46aca8..af12f74214 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts @@ -10,8 +10,35 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceViews: Array = []; -const workspaceActions: Array = []; +const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.MemberGroup.Info', + name: 'Member Group Workspace Info View', + loader: () => import('./views/info/workspace-view-member-group-info.element'), + weight: 90, + meta: { + workspaces: ['Umb.Workspace.MemberGroup'], + label: 'Info', + pathname: 'info', + icon: 'info', + }, + }, +]; + +const workspaceActions: Array = [ + { + type: 'workspaceAction', + alias: 'Umb.WorkspaceAction.MemberGroup.Save', + name: 'Save Member Group Workspace Action', + loader: () => import('src/backoffice/shared/components/workspace/actions/save/workspace-action-node-save.element'), + meta: { + workspaces: ['Umb.Workspace.MemberGroup'], + look: 'primary', + color: 'positive', + }, + }, +]; export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts new file mode 100644 index 0000000000..64a2cb8b1e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -0,0 +1,27 @@ +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; +import { UmbMemberGroupDetailStore, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from '../member-group.detail.store'; + +const DefaultMemberGroupData = { + key: '', + name: '', + icon: '', + id: '', + type: 'member-group', + hasChildren: false, +} as MemberGroupDetails; + +export class UmbWorkspaceMemberGroupContext extends UmbWorkspaceContentContext< + MemberGroupDetails, + UmbMemberGroupDetailStore +> { + + setPropertyValue(alias: string, value: unknown): void { + return; + } + + constructor(host: UmbControllerHostInterface) { + super(host, DefaultMemberGroupData, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'memberGroup'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts index 202795a92d..8f726fb427 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts @@ -1,9 +1,17 @@ +import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html, LitElement } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { css, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { distinctUntilChanged } from 'rxjs'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbWorkspaceMemberGroupContext } from './member-group-workspace.context'; +/** + * @element umb-member-group-workspace + * @description - Element for displaying a Member Group Workspace + */ @customElement('umb-member-group-workspace') -export class UmbMemberGroupWorkspaceElement extends LitElement { +export class UmbMemberGroupWorkspaceElement extends UmbLitElement { static styles = [ UUITextStyles, css` @@ -12,15 +20,63 @@ export class UmbMemberGroupWorkspaceElement extends LitElement { width: 100%; height: 100%; } + + #header { + /* TODO: can this be applied from layout slot CSS? */ + margin: 0 var(--uui-size-layout-1); + flex: 1 1 auto; + } `, ]; + private _entityKey!: string; @property() - id!: string; + public get entityKey(): string { + return this._entityKey; + } + public set entityKey(value: string) { + this._entityKey = value; + if (this._entityKey) { + this._workspaceContext.load(this._entityKey); + } + } + + @property() + public set create(parentKey: string | null) { + this._workspaceContext.create(parentKey); + } + + private _workspaceContext: UmbWorkspaceMemberGroupContext = new UmbWorkspaceMemberGroupContext(this); + + @state() + private _memberGroupName = ''; + + constructor() { + super(); + this.provideContext('umbWorkspaceContext', this._workspaceContext); + this.observe(this._workspaceContext.data.pipe(distinctUntilChanged()), (memberGroup) => { + if (memberGroup && memberGroup.name !== this._memberGroupName) { + this._memberGroupName = memberGroup.name ?? ''; + } + }); + } + + // TODO. find a way where we don't have to do this for all Workspaces. + private _handleInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this._workspaceContext.update({ name: target.value }); + } + } + } render() { return html` - Member Group Workspace + + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.stories.ts new file mode 100644 index 0000000000..844cd022ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.stories.ts @@ -0,0 +1,18 @@ +import './member-group-workspace.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import { data } from '../../../../core/mocks/data/member-group.data'; + +import type { UmbMemberGroupWorkspaceElement } from './member-group-workspace.element'; + +export default { + title: 'Workspaces/Member Group', + component: 'umb-member-group-workspace', + id: 'umb-member-group-workspace', +} as Meta; + +export const AAAOverview: Story = () => + html` `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts new file mode 100644 index 0000000000..6a8f56c8ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts @@ -0,0 +1,92 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { distinctUntilChanged } from 'rxjs'; +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbWorkspaceMemberGroupContext } from '../../member-group-workspace.context'; + +@customElement('umb-workspace-view-member-group-info') +export class UmbWorkspaceViewMemberGroupInfoElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + margin: var(--uui-size-layout-1); + gap: var(--uui-size-layout-1); + justify-content: space-between; + } + + uui-box { + margin-bottom: var(--ui-size-layout-1); + } + + uui-box:first-child { + flex: 1 1 75%; + } + + uui-box:last-child { + min-width: 320px; + } + `, + ]; + + @state() + _memberGroup?: MemberGroupDetails; + + private _workspaceContext?: UmbWorkspaceMemberGroupContext; + + 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', (memberGroupContext) => { + this._workspaceContext = memberGroupContext; + this._observeDataType(); + }); + } + + private _observeDataType() { + if (!this._workspaceContext) return; + + this.observe(this._workspaceContext.data.pipe(distinctUntilChanged()), (memberGroup) => { + if (!memberGroup) return; + + // TODO: handle if model is not of the type wanted. + // TODO: Make method to identify wether data is of type MemberGroupDetails + this._memberGroup = memberGroup as MemberGroupDetails; + }); + } + + private _renderGeneralInfo() { + return html` + + +
${this._memberGroup?.key}
+
+
+ `; + } + + // TODO => should use umb-empty-state when it exists + private _renderMemberGroupInfo() { + return html` + +

Member groups have no additional properties for editing.

+
+ `; + } + + render() { + return html` ${this._renderMemberGroupInfo()}${this._renderGeneralInfo()} `; + } +} + +export default UmbWorkspaceViewMemberGroupInfoElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-view-member-group-info': UmbWorkspaceViewMemberGroupInfoElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts new file mode 100644 index 0000000000..e2b88ed852 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts @@ -0,0 +1,27 @@ +import './workspace-view-member-group-info.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +//import { data } from '../../../../../core/mocks/data/data-type.data'; +//import { UmbDataTypeContext } from '../../data-type.context'; + +import type { UmbWorkspaceViewDataTypeInfoElement } from './workspace-view-member-group-info.element'; + +export default { + title: 'Workspaces/Data Type/Views/Info', + component: 'umb-workspace-view-data-type-info', + id: 'umb-workspace-view-data-type-info', + decorators: [ + (story) => { + return html`TODO: make use of mocked workspace context??`; + /*html` + ${story()} + `,*/ + } + ], +} as Meta; + +export const AAAOverview: Story = () => + html` `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts index c0142b6ef4..82c6706f0d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts @@ -17,11 +17,18 @@ export class UmbWorkspacePropertyLayoutElement extends LitElement { display: grid; grid-template-columns: 200px 600px; gap: var(--uui-size-layout-2); - } - :host { border-bottom: 1px solid var(--uui-color-divider); padding: var(--uui-size-space-6) 0; } + + :host([orientation="vertical"]) { + display:block; + } + + :host(:last-of-type) { + border-bottom:none; + } + p { margin-bottom: 0; } @@ -42,6 +49,9 @@ export class UmbWorkspacePropertyLayoutElement extends LitElement { @property({ type: String }) public label = ''; + @property({ type: String }) + public orientation: 'horizontal' | 'vertical' = 'horizontal'; + /** * Description: render a description underneath the label. * @type {string} From 9d2c28428860f28fdd76dfec1f2b0507b32f75f9 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 30 Jan 2023 12:36:03 +1000 Subject: [PATCH 3/9] merge main --- .../member-group.detail.store.ts | 25 +++++----- .../member-group.details.store.ts | 39 --------------- .../member-groups/member-group.tree.store.ts | 37 +++++++------- .../member-group-workspace.context.ts | 48 +++++++++++-------- .../member-group-workspace.element.ts | 2 +- 5 files changed, 57 insertions(+), 94 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts index de84905958..439f740cd5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -3,9 +3,7 @@ import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbContextToken } from '@umbraco-cms/context-api'; import { ArrayState, createObservablePart } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbStoreBase } from '@umbraco-cms/store'; -import { DictionaryItem, MemberGroupResource } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; import { umbMemberGroupData } from 'src/core/mocks/data/member-group.data'; export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupDetailStore'); @@ -16,17 +14,20 @@ export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken([], x => x.key); - public groups = this.#groups.asObservable(); +export class UmbMemberGroupDetailStore extends UmbStoreBase implements UmbEntityDetailStore { + #data = new ArrayState([], x => x.key); + public groups = this.#data.asObservable(); constructor(private host: UmbControllerHostInterface) { super(host, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString()); } + getScaffold(entityType: string, parentKey: string | null) { + return { + } as MemberGroupDetails; + } + /** * @description - Request a Member Group by key. The Member Group is added to the store and is returned as an Observable. * @param {string} key @@ -36,23 +37,23 @@ export class UmbMemberGroupDetailStore extends UmbStoreBase { getByKey(key: string): Observable { // tryExecuteAndNotify(this.host, MemberGroupResource.getMemberGroupByKey({ key })).then(({ data }) => { // if (data) { - // this.#groups.appendOne(data); + // this.#data.appendOne(data); // } // }); // temp until Resource is updated const group = umbMemberGroupData.getByKey(key); if (group) { - this.#groups.appendOne(group); + this.#data.appendOne(group); } return createObservablePart( - this.#groups, + this.#data, (groups) => groups.find((group) => group.key === key) as MemberGroupDetails ); } - async save(mediaTypes: Array): Promise { + async save(memberGroups: Array): Promise { return null as any; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts deleted file mode 100644 index 2189c2c274..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Observable } from 'rxjs'; -import type { MemberGroupDetails } from '@umbraco-cms/models'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; - -export const UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupStore'); - -/** - * @export - * @class UmbMemberGroupStore - * @extends {UmbStoreBase} - * @description - Data Store for Member Groups - */ -export class UmbMemberGroupStore extends UmbStoreBase implements UmbEntityDetailStore { - - - #groups = new ArrayState([], x => x.key); - public groups = this.#groups.asObservable(); - - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString()); - } - - getScaffold(entityType: string, parentKey: string | null) { - return { - } as MemberGroupDetails; - } - - getByKey(key: string): Observable { - return null as any; - } - - async save(memberGroups: Array): Promise { - return null as any; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts index 19807891a1..faf1ec5320 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts @@ -1,14 +1,13 @@ import { EntityTreeItem, MemberGroupResource, } from '@umbraco-cms/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { createObservablePart, ArrayState } from '@umbraco-cms/observable-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbStoreBase } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupTreeStore'); - /** * @export * @class UmbMemberGroupTreeStore @@ -17,11 +16,9 @@ export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.key); - constructor(host: UmbControllerHostInterface) { super(host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString()); } @@ -55,25 +52,23 @@ export class UmbMemberGroupTreeStore extends UmbStoreBase { }); // TODO: remove ignore when we know how to handle trashed items. - return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null)); + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); } - getTreeItemChildren(key: string) { - /* - tryExecuteAndNotify( - this._host, - MemberTypeResource.getTreeMemberTypeChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data.items); - } - }); - */ + getTreeItemChildren(key: string) { + // tryExecuteAndNotify( + // this._host, + // MemberGroupResource.getTreeMemberGroupChildren({ + // parentKey: key, + // }) + // ).then(({ data }) => { + // if (data) { + // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + // this.#data.append(data.items); + // } + // }); - return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key)); + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key)); } getTreeItems(keys: Array) { @@ -91,6 +86,6 @@ export class UmbMemberGroupTreeStore extends UmbStoreBase { }); } - return createObservablePart(this.#data, (items) => items.filter((item) => keys.includes(item.key ?? ''))); + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts index 64a2cb8b1e..20619f40bb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -1,27 +1,33 @@ import type { MemberGroupDetails } from '@umbraco-cms/models'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; -import { UmbMemberGroupDetailStore, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from '../member-group.detail.store'; +import { UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from '../member-group.detail.store'; +import { UmbWorkspaceEntityContextInterface } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface'; +import { UmbEntityWorkspaceManager } from 'src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller'; +import { UmbWorkspaceContext } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-context'; -const DefaultMemberGroupData = { - key: '', - name: '', - icon: '', - id: '', - type: 'member-group', - hasChildren: false, -} as MemberGroupDetails; +export class UmbWorkspaceMemberGroupContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ + #manager = new UmbEntityWorkspaceManager(this._host, 'memberGroup', UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN); -export class UmbWorkspaceMemberGroupContext extends UmbWorkspaceContentContext< - MemberGroupDetails, - UmbMemberGroupDetailStore -> { - - setPropertyValue(alias: string, value: unknown): void { - return; - } + public readonly data = this.#manager.state.asObservable(); + public readonly name = this.#manager.state.getObservablePart((state) => state?.name); - constructor(host: UmbControllerHostInterface) { - super(host, DefaultMemberGroupData, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'memberGroup'); + setPropertyValue(alias: string, value: string) { + return; } + + setName(name: string) { + this.#manager.state.update({name}); + } + + getEntityType = this.#manager.getEntityType; + getUnique = this.#manager.getEntityKey; + getEntityKey = this.#manager.getEntityKey; + getStore = this.#manager.getStore; + getData = this.#manager.getData; + load = this.#manager.load; + create = this.#manager.create; + save = this.#manager.save; + destroy = this.#manager.destroy; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts index 8f726fb427..3f3060c2e9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts @@ -67,7 +67,7 @@ export class UmbMemberGroupWorkspaceElement extends UmbLitElement { const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { - this._workspaceContext.update({ name: target.value }); + this._workspaceContext.setName(target.value); } } } From 18d12f0e0f95a69422366bacb98a526a8b76d3a4 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Tue, 7 Feb 2023 14:19:54 +1000 Subject: [PATCH 4/9] implement workspace interface --- .../member-group-workspace.context.ts | 2 +- .../member-group-workspace.element.ts | 27 +++++++------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts index 20619f40bb..7f306a8a8f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -1,5 +1,5 @@ -import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from '../member-group.detail.store'; +import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbWorkspaceEntityContextInterface } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface'; import { UmbEntityWorkspaceManager } from 'src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller'; import { UmbWorkspaceContext } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-context'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts index 3f3060c2e9..039d85a758 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.element.ts @@ -1,17 +1,18 @@ import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; import { distinctUntilChanged } from 'rxjs'; -import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbWorkspaceEntityElement } from '../../../../backoffice/shared/components/workspace/workspace-entity-element.interface'; import { UmbWorkspaceMemberGroupContext } from './member-group-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/element'; /** * @element umb-member-group-workspace * @description - Element for displaying a Member Group Workspace */ @customElement('umb-member-group-workspace') -export class UmbMemberGroupWorkspaceElement extends UmbLitElement { +export class UmbMemberGroupWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { static styles = [ UUITextStyles, css` @@ -28,32 +29,22 @@ export class UmbMemberGroupWorkspaceElement extends UmbLitElement { } `, ]; + private _workspaceContext: UmbWorkspaceMemberGroupContext = new UmbWorkspaceMemberGroupContext(this); - private _entityKey!: string; - @property() - public get entityKey(): string { - return this._entityKey; - } - public set entityKey(value: string) { - this._entityKey = value; - if (this._entityKey) { - this._workspaceContext.load(this._entityKey); - } + public load(entityKey: string) { + this._workspaceContext.load(entityKey); } - @property() - public set create(parentKey: string | null) { + public create(parentKey: string | null) { this._workspaceContext.create(parentKey); } - private _workspaceContext: UmbWorkspaceMemberGroupContext = new UmbWorkspaceMemberGroupContext(this); - @state() private _memberGroupName = ''; constructor() { super(); - this.provideContext('umbWorkspaceContext', this._workspaceContext); + this.observe(this._workspaceContext.data.pipe(distinctUntilChanged()), (memberGroup) => { if (memberGroup && memberGroup.name !== this._memberGroupName) { this._memberGroupName = memberGroup.name ?? ''; From 4b2b4e412cbeac6241d8c5a620f15ed1c2548c01 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Wed, 8 Feb 2023 13:56:01 +1000 Subject: [PATCH 5/9] clean up imports --- .../member-group.detail.store.ts | 27 +++++++++++-------- .../member-group-workspace.context.ts | 6 ++--- ...orkspace-view-member-group-info.element.ts | 8 +++--- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts index 439f740cd5..9050bb1314 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -1,10 +1,9 @@ import { Observable } from 'rxjs'; import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState, createObservablePart } from '@umbraco-cms/observable-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; -import { umbMemberGroupData } from 'src/core/mocks/data/member-group.data'; export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupDetailStore'); @@ -34,22 +33,28 @@ export class UmbMemberGroupDetailStore extends UmbStoreBase implements UmbEntity * @return {*} {(Observable)} * @memberof UmbMemberGroupDetailStore */ - getByKey(key: string): Observable { + getByKey(key: string): Observable { // tryExecuteAndNotify(this.host, MemberGroupResource.getMemberGroupByKey({ key })).then(({ data }) => { // if (data) { // this.#data.appendOne(data); // } // }); - // temp until Resource is updated - const group = umbMemberGroupData.getByKey(key); - if (group) { - this.#data.appendOne(group); - } + // return createObservablePart( + // this.#data, + // (groups) => groups.find((group) => group.key === key) as MemberGroupDetails + // ); - return createObservablePart( - this.#data, - (groups) => groups.find((group) => group.key === key) as MemberGroupDetails + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/member-group/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + console.log(data); + }); + + return this.#data.getObservablePart((memberGroups) => + memberGroups.find((memberGroup) => memberGroup.key === key) ); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts index 7f306a8a8f..5ea4fc3f43 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -1,8 +1,8 @@ import { UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from '../member-group.detail.store'; +import { UmbWorkspaceEntityContextInterface } from '../../../../backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface'; +import { UmbEntityWorkspaceManager } from '../../../../backoffice/shared/components/workspace/workspace-context/entity-manager-controller'; +import { UmbWorkspaceContext } from '../../../../backoffice/shared/components/workspace/workspace-context/workspace-context'; import type { MemberGroupDetails } from '@umbraco-cms/models'; -import { UmbWorkspaceEntityContextInterface } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface'; -import { UmbEntityWorkspaceManager } from 'src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller'; -import { UmbWorkspaceContext } from 'src/backoffice/shared/components/workspace/workspace-context/workspace-context'; export class UmbWorkspaceMemberGroupContext extends UmbWorkspaceContext diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts index 6a8f56c8ea..0f81ac494d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts @@ -2,9 +2,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { distinctUntilChanged } from 'rxjs'; +import { UmbWorkspaceMemberGroupContext } from '../../member-group-workspace.context'; import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; -import { UmbWorkspaceMemberGroupContext } from '../../member-group-workspace.context'; @customElement('umb-workspace-view-member-group-info') export class UmbWorkspaceViewMemberGroupInfoElement extends UmbLitElement { @@ -43,14 +43,16 @@ export class UmbWorkspaceViewMemberGroupInfoElement extends UmbLitElement { // 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', (memberGroupContext) => { this._workspaceContext = memberGroupContext; - this._observeDataType(); + console.log(memberGroupContext); + this._observeMemberGroup(); }); } - private _observeDataType() { + private _observeMemberGroup() { if (!this._workspaceContext) return; this.observe(this._workspaceContext.data.pipe(distinctUntilChanged()), (memberGroup) => { + console.log(memberGroup); if (!memberGroup) return; // TODO: handle if model is not of the type wanted. From 8374d0d04e6cd4906bb8a5b6f0bb980afa9fb04b Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Wed, 8 Feb 2023 15:16:01 +1000 Subject: [PATCH 6/9] scaffold member tree/context/workspace etc with data/handlers as no API exists yet --- .../workspace/member-workspace.context.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts new file mode 100644 index 0000000000..d010b9edd0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts @@ -0,0 +1,33 @@ +import { UmbEntityWorkspaceManager } from '../../../shared/components/workspace/workspace-context/entity-manager-controller'; +import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; +import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; +import { UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN } from '../member.detail.store'; +import type { MemberDetails } from '@umbraco-cms/models'; + +export class UmbWorkspaceMemberContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ + #manager = new UmbEntityWorkspaceManager(this._host, 'member', UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN); + + public readonly data = this.#manager.state.asObservable(); + public readonly name = this.#manager.state.getObservablePart((state) => state?.name); + + setPropertyValue(alias: string, value: string) { + return; + } + + setName(name: string) { + this.#manager.state.update({name}); + } + + getEntityType = this.#manager.getEntityType; + getUnique = this.#manager.getEntityKey; + getEntityKey = this.#manager.getEntityKey; + getStore = this.#manager.getStore; + getData = this.#manager.getData; + load = this.#manager.load; + create = this.#manager.create; + save = this.#manager.save; + destroy = this.#manager.destroy; +} From e4effeb3a73ffacfd77a8c435925462b4cee52c8 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Wed, 8 Feb 2023 15:24:46 +1000 Subject: [PATCH 7/9] scaffold members section --- .../libs/models/index.ts | 4 + .../src/backoffice/backoffice.element.ts | 4 + .../member-group.detail.store.ts | 27 ++--- .../member-groups/member-group.tree.store.ts | 1 + .../action-member-group-delete.element.ts | 15 ++- .../members/member-groups/tree/manifests.ts | 2 +- ...orkspace-view-member-group-info.element.ts | 2 - .../members/members/member.detail.store.ts | 59 ++++++++++ .../members/members/member.tree.store.ts | 102 ++++++++++++++++++ .../members/sidebar-menu-item/manifests.ts | 1 + .../members-sidebar-menu-item.element.ts | 3 +- .../actions/action-member-delete.element.ts | 54 ++++++++++ .../members/members/tree/manifests.ts | 21 +++- .../workspace/member-workspace.stories.ts | 18 ++++ .../src/core/mocks/browser-handlers.ts | 2 + .../src/core/mocks/data/member-group.data.ts | 6 +- .../src/core/mocks/data/member.data.ts | 47 ++++++++ .../src/core/mocks/domains/member.handlers.ts | 19 ++++ 18 files changed, 352 insertions(+), 35 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/mocks/data/member.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/mocks/domains/member.handlers.ts diff --git a/src/Umbraco.Web.UI.Client/libs/models/index.ts b/src/Umbraco.Web.UI.Client/libs/models/index.ts index 9e91bcb3df..3658a16304 100644 --- a/src/Umbraco.Web.UI.Client/libs/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/models/index.ts @@ -137,6 +137,10 @@ export interface MemberGroupDetails extends EntityTreeItem { key: string; // TODO: Remove this when the backend is fixed } +export interface MemberDetails extends EntityTreeItem { + key: string; // TODO: Remove this when the backend is fixed +} + // Dictionary export interface DictionaryDetails extends EntityTreeItem { key: string; // TODO: Remove this when the backend is fixed diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 7c1d8e3c72..522abf3596 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -27,6 +27,8 @@ import { UmbMemberTypeDetailStore } from './members/member-types/member-type.det import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store'; import { UmbMemberGroupDetailStore } from './members/member-groups/member-group.detail.store'; import { UmbMemberGroupTreeStore } from './members/member-groups/member-group.tree.store'; +import { UmbMemberDetailStore } from './members/members/member.detail.store'; +import { UmbMemberTreeStore } from './members/members/member.tree.store'; import { UmbDictionaryDetailStore } from './translation/dictionary/dictionary.detail.store'; import { UmbDictionaryTreeStore } from './translation/dictionary/dictionary.tree.store'; import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store'; @@ -96,6 +98,8 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbUserGroupStore(this); new UmbMemberGroupDetailStore(this); new UmbMemberGroupTreeStore(this); + new UmbMemberDetailStore(this); + new UmbMemberTreeStore(this); new UmbDictionaryDetailStore(this); new UmbDictionaryTreeStore(this); new UmbDocumentBlueprintDetailStore(this); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts index 9050bb1314..bbf4f61c2d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -1,7 +1,8 @@ import { Observable } from 'rxjs'; +import { umbMemberGroupData } from '../../../core/mocks/data/member-group.data'; import type { MemberGroupDetails } from '@umbraco-cms/models'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; +import { ArrayState, createObservablePart } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; @@ -33,28 +34,22 @@ export class UmbMemberGroupDetailStore extends UmbStoreBase implements UmbEntity * @return {*} {(Observable)} * @memberof UmbMemberGroupDetailStore */ - getByKey(key: string): Observable { + getByKey(key: string): Observable { // tryExecuteAndNotify(this.host, MemberGroupResource.getMemberGroupByKey({ key })).then(({ data }) => { // if (data) { // this.#data.appendOne(data); // } // }); - // return createObservablePart( - // this.#data, - // (groups) => groups.find((group) => group.key === key) as MemberGroupDetails - // ); + // temp until Resource is updated + const group = umbMemberGroupData.getByKey(key); + if (group) { + this.#data.appendOne(group); + } - // TODO: use backend cli when available. - fetch(`/umbraco/management/api/v1/member-group/${key}`) - .then((res) => res.json()) - .then((data) => { - this.#data.append(data); - console.log(data); - }); - - return this.#data.getObservablePart((memberGroups) => - memberGroups.find((memberGroup) => memberGroup.key === key) + return createObservablePart( + this.#data, + (groups) => groups.find((group) => group.key === key) as MemberGroupDetails ); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts index faf1ec5320..87470e4dad 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts @@ -48,6 +48,7 @@ export class UmbMemberGroupTreeStore extends UmbStoreBase { if (data) { // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? this.#data.append(data.items); + console.log(this.#data); } }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts index 231fda58ce..9c4304f05b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts @@ -3,17 +3,14 @@ import { css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; -import { - UmbMemberGroupDetailStore, - UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN, -} from '../../member-group.detail.store'; +import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../../member-group.tree.store'; @customElement('umb-tree-action-member-group-delete') export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemActionElement { static styles = [UUITextStyles, css``]; private _modalService?: UmbModalService; - private _memberGroupDetailStore?: UmbMemberGroupDetailStore; + private _memberGroupTreeStore?: UmbMemberGroupTreeStore; connectedCallback(): void { super.connectedCallback(); @@ -22,8 +19,8 @@ export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemAc this._modalService = modalService; }); - this.consumeContext(UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN, (memberGroupDetailStore) => { - this._memberGroupDetailStore = memberGroupDetailStore; + this.consumeContext(UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN, (memberGroupTreeStore) => { + this._memberGroupTreeStore = memberGroupTreeStore; }); } @@ -36,8 +33,8 @@ export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemAc }); modalHandler?.onClose().then(({ confirmed }: any) => { - if (confirmed && this._treeContextMenuService && this._memberGroupDetailStore && this._activeTreeItem) { - this._memberGroupDetailStore?.trash([this._activeTreeItem.key]); + if (confirmed && this._treeContextMenuService && this._memberGroupTreeStore && this._activeTreeItem) { + this._memberGroupTreeStore?.delete([this._activeTreeItem.key]); this._treeContextMenuService.close(); } }); 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 ede9dff6de..d39cc39eab 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 type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; import { UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../member-group.tree.store'; +import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.MemberGroups'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts index 0f81ac494d..cc2053fd03 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.element.ts @@ -43,7 +43,6 @@ export class UmbWorkspaceViewMemberGroupInfoElement extends UmbLitElement { // 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', (memberGroupContext) => { this._workspaceContext = memberGroupContext; - console.log(memberGroupContext); this._observeMemberGroup(); }); } @@ -52,7 +51,6 @@ export class UmbWorkspaceViewMemberGroupInfoElement extends UmbLitElement { if (!this._workspaceContext) return; this.observe(this._workspaceContext.data.pipe(distinctUntilChanged()), (memberGroup) => { - console.log(memberGroup); if (!memberGroup) return; // TODO: handle if model is not of the type wanted. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts new file mode 100644 index 0000000000..bb665251ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts @@ -0,0 +1,59 @@ +import { Observable } from 'rxjs'; +import type { MemberDetails, MemberGroupDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState, createObservablePart } from '@umbraco-cms/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; +import { umbMemberData } from 'src/core/mocks/data/member.data'; + +export const UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberDetailStore'); + +/** + * @export + * @class UmbMemberDetailStore + * @extends {UmbStoreBase} + * @description - Detail Data Store for Members + */ +export class UmbMemberDetailStore extends UmbStoreBase implements UmbEntityDetailStore { + + #data = new ArrayState([], x => x.key); + public groups = this.#data.asObservable(); + + constructor(private host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + getScaffold(entityType: string, parentKey: string | null) { + return { + } as MemberDetails; + } + + /** + * @description - Request a Member by key. The Member is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbMemberDetailStore + */ + getByKey(key: string): Observable { + // tryExecuteAndNotify(this.host, MemberResource.getMemberByKey({ key })).then(({ data }) => { + // if (data) {} + // this.#data.appendOne(data); + // } + // }); + + // temp until Resource is updated + const member = umbMemberData.getByKey(key); + if (member) { + this.#data.appendOne(member); + } + + return createObservablePart( + this.#data, + (members) => members.find((member) => member.key === key) as MemberDetails + ); + } + + async save(member: Array): Promise { + return null as any; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts new file mode 100644 index 0000000000..ee3c7a5e42 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts @@ -0,0 +1,102 @@ +import { EntityTreeItem, MemberGroupResource } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTreeStore'); + +/** + * @export + * @class UmbMemberTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Members + */ +export class UmbMemberTreeStore extends UmbStoreBase { + // TODO: use the right type here: + #data = new ArrayState([], (x) => x.key); + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Member. + * @param {string[]} keys + * @memberof UmbMemberTreeStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/members/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + async getTreeRoot() { + // TODO: use backend cli when available. + tryExecuteAndNotify( + this._host, + fetch('/umbraco/management/api/v1/tree/member/root') + .then((res) => res.json()) + .then((data) => { + this.#data.append(data.items); + debugger; + }) + ); + + console.log(this.#data); + + // tryExecuteAndNotify(this._host, MembersResource.getTreeMembersRoot({})).then(({ data }) => { + // if (data) { + // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + // this.#data.append(data.items); + // } + // }); + + // TODO: remove ignore when we know how to handle trashed items. + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); + } + + getTreeItemChildren(key: string) { + // tryExecuteAndNotify( + // this._host, + // MembersResource.getTreeMembersChildren({ + // parentKey: key, + // }) + // ).then(({ data }) => { + // if (data) { + // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + // this.#data.append(data.items); + // } + // }); + + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key)); + } + + getTreeItems(keys: Array) { + // if (keys?.length > 0) { + // tryExecuteAndNotify( + // this._host, + // MembersResource.getTreeMembersItem({ + // key: keys, + // }) + // ).then(({ data }) => { + // if (data) { + // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? + // this.#data.append(data); + // } + // }); + // } + + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/manifests.ts index 83ec74ac8c..e1afd744ff 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/manifests.ts @@ -10,6 +10,7 @@ const sidebarMenuItem: ManifestSidebarMenuItem = { label: 'Members', icon: 'umb:folder', sections: ['Umb.Section.Members'], + entityType: 'member', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/members-sidebar-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/members-sidebar-menu-item.element.ts index 3a9f7e7e87..8ab8e3b82e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/members-sidebar-menu-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/sidebar-menu-item/members-sidebar-menu-item.element.ts @@ -23,7 +23,8 @@ export class UmbMembersSidebarMenuItemElement extends UmbLitElement { label="Members" icon="umb:folder" @show-children=${this._onShowChildren} - @hide-children=${this._onHideChildren}> + @hide-children=${this._onHideChildren} + has-children> ${this._renderTree ? html`` : nothing} `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts new file mode 100644 index 0000000000..e0e1a909c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts @@ -0,0 +1,54 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; +import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; +import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../../member.tree.store'; + +@customElement('umb-tree-action-member-delete') +export default class UmbTreeActionMemberDeleteElement extends UmbTreeItemActionElement { + static styles = [UUITextStyles, css``]; + + private _modalService?: UmbModalService; + private _memberTreeStore?: UmbMemberTreeStore; + + connectedCallback(): void { + super.connectedCallback(); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (modalService) => { + this._modalService = modalService; + }); + + this.consumeContext(UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN, (memberTreeStore) => { + this._memberTreeStore = memberTreeStore; + }); + } + + private _handleLabelClick() { + const modalHandler = this._modalService?.confirm({ + headline: `Delete ${this._activeTreeItem?.name ?? 'item'}`, + content: 'Are you sure you want to delete this item?', + color: 'danger', + confirmLabel: 'Delete', + }); + + modalHandler?.onClose().then(({ confirmed }: any) => { + if (confirmed && this._treeContextMenuService && this._memberTreeStore && this._activeTreeItem) { + this._memberTreeStore?.delete([this._activeTreeItem.key]); + this._treeContextMenuService.close(); + } + }); + } + + render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree-action-member-delete': UmbTreeActionMemberDeleteElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts index c3f4d051af..d049dadb75 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts @@ -1,14 +1,29 @@ +import { UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../member.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { type: 'tree', alias: 'Umb.Tree.Members', name: 'Members Tree', + weight: 10, meta: { - storeAlias: 'umbMemberTypesStore', + storeAlias: UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; -const treeItemActions: Array = []; +const treeItemActions: Array = [ + { + type: 'treeItemAction', + alias: 'Umb.TreeItemAction.Member.Delete', + name: 'Member Tree Item Action Delete', + loader: () => import('./actions/action-member-delete.element'), + weight: 100, + meta: { + entityType: 'member', + label: 'Delete', + icon: 'delete', + }, + }, +]; -export const manifests = [tree, ...treeItemActions]; +export const manifests = [tree, ...treeItemActions]; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.stories.ts new file mode 100644 index 0000000000..4a9f3778ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.stories.ts @@ -0,0 +1,18 @@ +import './member-workspace.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import { data } from '../../../../core/mocks/data/member.data'; + +import type { UmbMemberWorkspaceElement } from './member-workspace.element'; + +export default { + title: 'Workspaces/Member', + component: 'umb-member-workspace', + id: 'umb-member-workspace', +} as Meta; + +export const AAAOverview: Story = () => + html` `; +AAAOverview.storyName = 'Overview'; 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 ff8ad5b646..231eb8291c 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 @@ -18,6 +18,7 @@ import { handlers as mediaHandlers } from './domains/media.handlers'; import { handlers as dictionaryHandlers } from './domains/dictionary.handlers'; import { handlers as mediaTypeHandlers } from './domains/media-type.handlers'; import { handlers as memberGroupHandlers } from './domains/member-group.handlers'; +import { handlers as memberHandlers } from './domains/member.handlers'; import { handlers as memberTypeHandlers } from './domains/member-type.handlers'; import { handlers as templateHandlers } from './domains/template.handlers'; import { handlers as languageHandlers } from './domains/language.handlers'; @@ -39,6 +40,7 @@ const handlers = [ ...userGroupsHandlers, ...mediaTypeHandlers, ...memberGroupHandlers, + ...memberHandlers, ...memberTypeHandlers, ...examineManagementHandlers, ...modelsBuilderHandlers, diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts index 19f363423d..005438157c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member-group.data.ts @@ -1,11 +1,11 @@ -import type { MemberGroupDetails } from '@umbraco-cms/models'; -import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; import { UmbEntityData } from './entity.data'; import { createEntityTreeItem } from './utils'; +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; export const data: Array = [ { - name: 'Member Group 1', + name: 'Member Group AAA', type: 'member-group', icon: 'umb:document', hasChildren: false, diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/member.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member.data.ts new file mode 100644 index 0000000000..612475bc6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/member.data.ts @@ -0,0 +1,47 @@ +import { UmbEntityData } from './entity.data'; +import { createEntityTreeItem } from './utils'; +import type { MemberDetails } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; + +export const data: Array = [ + { + name: 'Member AAA', + type: 'member', + icon: 'umb:user', + hasChildren: false, + key: 'aaa08ccd-4179-464c-b634-6969149dd9f9', + isContainer: false, + parentKey: null, + }, +]; + +// 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 UmbMemberData extends UmbEntityData { + constructor() { + super(data); + } + + getTreeRoot(): PagedEntityTreeItem { + const items = this.data.filter((item) => item.parentKey === null); + const treeItems = items.map((item) => createEntityTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItemChildren(key: string): PagedEntityTreeItem { + const items = this.data.filter((item) => item.parentKey === key); + const treeItems = items.map((item) => createEntityTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItem(keys: Array): Array { + const items = this.data.filter((item) => keys.includes(item.key ?? '')); + return items.map((item) => createEntityTreeItem(item)); + } +} + +export const umbMemberData = new UmbMemberData(); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/member.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/member.handlers.ts new file mode 100644 index 0000000000..41555f9b2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/member.handlers.ts @@ -0,0 +1,19 @@ +import { rest } from 'msw'; +import { umbMemberData } from '../data/member.data'; + +// TODO: add schema +export const handlers = [ + rest.get('/umbraco/management/api/v1/tree/member/root', (req, res, ctx) => { + const response = umbMemberData.getTreeRoot(); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get('/umbraco/management/api/v1/tree/member/item', (req, res, ctx) => { + const keys = req.url.searchParams.getAll('key'); + if (!keys) return; + + const items = umbMemberData.getTreeItem(keys); + + return res(ctx.status(200), ctx.json(items)); + }), +]; From b4a08958c9e265b843f7f61340f0e7acf4eff268 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 9 Feb 2023 10:05:18 +1000 Subject: [PATCH 8/9] tree repositories - members and member groups --- .../src/backoffice/backoffice.element.ts | 4 +- .../member-groups/member-group.tree.store.ts | 92 ---------------- .../tree/data/member-group.tree.repository.ts | 88 +++++++++++++++ .../tree/data/member-group.tree.store.ts | 95 ++++++++++++++++ .../member-groups/tree/data/sources/index.ts | 7 ++ .../sources/member-group.tree.server.data.ts | 52 +++++++++ .../members/member-groups/tree/manifests.ts | 4 +- .../members/members/member.tree.store.ts | 102 ------------------ .../actions/action-member-delete.element.ts | 2 +- .../tree/data/member.tree.repository.ts | 88 +++++++++++++++ .../members/tree/data/member.tree.store.ts | 95 ++++++++++++++++ .../members/tree/data/sources/index.ts | 7 ++ .../data/sources/member.tree.server.data.ts | 61 +++++++++++ .../members/members/tree/manifests.ts | 4 +- 14 files changed, 500 insertions(+), 201 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/member-group.tree.server.data.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/member.tree.server.data.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 522abf3596..bb99ec397e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -26,9 +26,9 @@ import { UmbMediaTreeStore } from './media/media/media.tree.store'; import { UmbMemberTypeDetailStore } from './members/member-types/member-type.detail.store'; import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store'; import { UmbMemberGroupDetailStore } from './members/member-groups/member-group.detail.store'; -import { UmbMemberGroupTreeStore } from './members/member-groups/member-group.tree.store'; +import { UmbMemberGroupTreeStore } from './members/member-groups/tree/data/member-group.tree.store'; import { UmbMemberDetailStore } from './members/members/member.detail.store'; -import { UmbMemberTreeStore } from './members/members/member.tree.store'; +import { UmbMemberTreeStore } from './members/members/tree/data/member.tree.store'; import { UmbDictionaryDetailStore } from './translation/dictionary/dictionary.detail.store'; import { UmbDictionaryTreeStore } from './translation/dictionary/dictionary.tree.store'; import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts deleted file mode 100644 index 87470e4dad..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.tree.store.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { EntityTreeItem, MemberGroupResource, } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; - - -export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupTreeStore'); - -/** - * @export - * @class UmbMemberGroupTreeStore - * @extends {UmbStoreBase} - * @description - Tree Data Store for Member Groups - */ -export class UmbMemberGroupTreeStore extends UmbStoreBase { - - // TODO: use the right type here: - #data = new ArrayState([], (x) => x.key); - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString()); - } - - // TODO: How can we avoid having this in both stores? - /** - * @description - Delete a Data Type. - * @param {string[]} keys - * @memberof UmbMemberGroupsStore - * @return {*} {Promise} - */ - async delete(keys: string[]) { - // TODO: use backend cli when available. - await fetch('/umbraco/backoffice/member-group/delete', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.#data.remove(keys); - } - - getTreeRoot() { - tryExecuteAndNotify(this._host, MemberGroupResource.getTreeMemberGroupRoot({})).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data.items); - console.log(this.#data); - } - }); - - // TODO: remove ignore when we know how to handle trashed items. - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); - } - - getTreeItemChildren(key: string) { - // tryExecuteAndNotify( - // this._host, - // MemberGroupResource.getTreeMemberGroupChildren({ - // parentKey: key, - // }) - // ).then(({ data }) => { - // if (data) { - // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - // this.#data.append(data.items); - // } - // }); - - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key)); - } - - getTreeItems(keys: Array) { - if (keys?.length > 0) { - tryExecuteAndNotify( - this._host, - MemberGroupResource.getTreeMemberGroupItem({ - key: keys, - }) - ).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data); - } - }); - } - - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.repository.ts new file mode 100644 index 0000000000..231676fa4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.repository.ts @@ -0,0 +1,88 @@ +import { MemberGroupTreeServerDataSource } from './sources/member-group.tree.server.data'; +import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from './member-group.tree.store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import type { UmbTreeRepository } from '@umbraco-cms/models'; + +export class UmbMemberGroupTreeRepository implements UmbTreeRepository { + #host: UmbControllerHostInterface; + #dataSource: MemberGroupTreeServerDataSource; + #treeStore?: UmbMemberGroupTreeStore; + #notificationService?: UmbNotificationService; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + // TODO: figure out how spin up get the correct data source + this.#dataSource = new MemberGroupTreeServerDataSource(this.#host); + + new UmbContextConsumerController(this.#host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + this.#checkIfInitialized(); + }); + } + + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + + #checkIfInitialized() { + if (this.#treeStore && this.#notificationService) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async requestRootItems() { + await this.#init; + + const { data, error } = await this.#dataSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error }; + } + + async requestChildrenOf(parentKey: string | null) { + const error: ProblemDetails = { title: 'Not implemented' }; + return { data: undefined, error }; + } + + async requestItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#dataSource.getItems(keys); + + return { data, error }; + } + + async rootItems() { + await this.#init; + return this.#treeStore!.rootItems(); + } + + async childrenOf(parentKey: string | null) { + await this.#init; + return this.#treeStore!.childrenOf(parentKey); + } + + async items(keys: Array) { + await this.#init; + return this.#treeStore!.items(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.store.ts new file mode 100644 index 0000000000..17d84a80ef --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/member-group.tree.store.ts @@ -0,0 +1,95 @@ +import { EntityTreeItem } from '@umbraco-cms/backend-api'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbMemberGroupTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Member Groups + */ +export class UmbMemberGroupTreeStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbTemplateTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbMemberGroupTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbTemplateTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} key + * @param {Partial} data + * @memberof UmbMemberGroupTreeStore + */ + updateItem(key: string, data: Partial) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.appendOne({ ...entry, ...data }); + } + } + + /** + * Removes an item from the store + * @param {string} key + * @memberof UmbMemberGroupTreeStore + */ + removeItem(key: string) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.remove([key]); + } + } + + /** + * Returns an observable to observe the root items + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + rootItems() { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); + } + + /** + * Returns an observable to observe the children of a given parent + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + childrenOf(parentKey: string | null) { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); + } + + /** + * Returns an observable to observe the items with the given keys + * @param {Array} keys + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + items(keys: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} + +export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbMemberGroupTreeStore.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/index.ts new file mode 100644 index 0000000000..28b9ebc0aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/index.ts @@ -0,0 +1,7 @@ +import type { DataSourceResponse } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; + +export interface MemberGroupTreeDataSource { + getRootItems(): Promise>; + getItems(key: Array): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/member-group.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/member-group.tree.server.data.ts new file mode 100644 index 0000000000..59802ee2b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/data/sources/member-group.tree.server.data.ts @@ -0,0 +1,52 @@ +import { MemberGroupTreeDataSource } from '.'; +import { MemberGroupResource, ProblemDetails } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Member Group tree that fetches data from the server + * @export + * @class MemberGroupTreeServerDataSource + * @implements {MemberGroupTreeDataSource} + */ +export class MemberGroupTreeServerDataSource implements MemberGroupTreeDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of MemberGroupTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof MemberGroupTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof MemberGroupTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({})); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof MemberGroupTreeServerDataSource + */ + async getItems(keys: Array) { + if (keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { error }; + } + + 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 d39cc39eab..3374e2e029 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,4 +1,4 @@ -import { UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../member-group.tree.store'; +import { UmbMemberGroupTreeRepository } from './data/member-group.tree.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.MemberGroups'; @@ -9,7 +9,7 @@ const tree: ManifestTree = { name: 'Member Groups Tree', weight: 100, meta: { - storeAlias: UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString(), + repository: UmbMemberGroupTreeRepository }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts deleted file mode 100644 index ee3c7a5e42..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.tree.store.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { EntityTreeItem, MemberGroupResource } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; - -export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTreeStore'); - -/** - * @export - * @class UmbMemberTreeStore - * @extends {UmbStoreBase} - * @description - Tree Data Store for Members - */ -export class UmbMemberTreeStore extends UmbStoreBase { - // TODO: use the right type here: - #data = new ArrayState([], (x) => x.key); - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString()); - } - - // TODO: How can we avoid having this in both stores? - /** - * @description - Delete a Member. - * @param {string[]} keys - * @memberof UmbMemberTreeStore - * @return {*} {Promise} - */ - async delete(keys: string[]) { - // TODO: use backend cli when available. - await fetch('/umbraco/backoffice/members/delete', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.#data.remove(keys); - } - - async getTreeRoot() { - // TODO: use backend cli when available. - tryExecuteAndNotify( - this._host, - fetch('/umbraco/management/api/v1/tree/member/root') - .then((res) => res.json()) - .then((data) => { - this.#data.append(data.items); - debugger; - }) - ); - - console.log(this.#data); - - // tryExecuteAndNotify(this._host, MembersResource.getTreeMembersRoot({})).then(({ data }) => { - // if (data) { - // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - // this.#data.append(data.items); - // } - // }); - - // TODO: remove ignore when we know how to handle trashed items. - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); - } - - getTreeItemChildren(key: string) { - // tryExecuteAndNotify( - // this._host, - // MembersResource.getTreeMembersChildren({ - // parentKey: key, - // }) - // ).then(({ data }) => { - // if (data) { - // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - // this.#data.append(data.items); - // } - // }); - - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key)); - } - - getTreeItems(keys: Array) { - // if (keys?.length > 0) { - // tryExecuteAndNotify( - // this._host, - // MembersResource.getTreeMembersItem({ - // key: keys, - // }) - // ).then(({ data }) => { - // if (data) { - // // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - // this.#data.append(data); - // } - // }); - // } - - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts index e0e1a909c0..97b73c2bf8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts @@ -3,7 +3,7 @@ import { css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; -import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../../member.tree.store'; +import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../data/member.tree.store'; @customElement('umb-tree-action-member-delete') export default class UmbTreeActionMemberDeleteElement extends UmbTreeItemActionElement { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.repository.ts new file mode 100644 index 0000000000..6f501df8da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.repository.ts @@ -0,0 +1,88 @@ +import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from './member.tree.store'; +import { MemberTreeServerDataSource } from './sources/member.tree.server.data'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import type { UmbTreeRepository } from '@umbraco-cms/models'; + +export class UmbMemberTreeRepository implements UmbTreeRepository { + #host: UmbControllerHostInterface; + #dataSource: MemberTreeServerDataSource; + #treeStore?: UmbMemberTreeStore; + #notificationService?: UmbNotificationService; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + // TODO: figure out how spin up get the correct data source + this.#dataSource = new MemberTreeServerDataSource(this.#host); + + new UmbContextConsumerController(this.#host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + this.#checkIfInitialized(); + }); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + this.#checkIfInitialized(); + }); + } + + #init = new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + + #checkIfInitialized() { + if (this.#treeStore && this.#notificationService) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async requestRootItems() { + await this.#init; + + const { data, error } = await this.#dataSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error }; + } + + async requestChildrenOf(parentKey: string | null) { + const error: ProblemDetails = { title: 'Not implemented' }; + return { data: undefined, error }; + } + + async requestItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#dataSource.getItems(keys); + + return { data, error }; + } + + async rootItems() { + await this.#init; + return this.#treeStore!.rootItems(); + } + + async childrenOf(parentKey: string | null) { + await this.#init; + return this.#treeStore!.childrenOf(parentKey); + } + + async items(keys: Array) { + await this.#init; + return this.#treeStore!.items(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.store.ts new file mode 100644 index 0000000000..6013b0e9cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/member.tree.store.ts @@ -0,0 +1,95 @@ +import { EntityTreeItem } from '@umbraco-cms/backend-api'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbMemberTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Members + */ +export class UmbMemberTreeStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbTemplateTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbMemberGroupTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbTemplateTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} key + * @param {Partial} data + * @memberof UmbMemberGroupTreeStore + */ + updateItem(key: string, data: Partial) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.appendOne({ ...entry, ...data }); + } + } + + /** + * Removes an item from the store + * @param {string} key + * @memberof UmbMemberGroupTreeStore + */ + removeItem(key: string) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.remove([key]); + } + } + + /** + * Returns an observable to observe the root items + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + rootItems() { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); + } + + /** + * Returns an observable to observe the children of a given parent + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + childrenOf(parentKey: string | null) { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); + } + + /** + * Returns an observable to observe the items with the given keys + * @param {Array} keys + * @return {*} + * @memberof UmbMemberGroupTreeStore + */ + items(keys: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} + +export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbMemberTreeStore.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/index.ts new file mode 100644 index 0000000000..8e446ee38c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/index.ts @@ -0,0 +1,7 @@ +import type { DataSourceResponse } from '@umbraco-cms/models'; +import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; + +export interface MemberTreeDataSource { + getRootItems(): Promise>; + getItems(key: Array): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/member.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/member.tree.server.data.ts new file mode 100644 index 0000000000..e4ef44db14 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/data/sources/member.tree.server.data.ts @@ -0,0 +1,61 @@ +import { MemberTreeDataSource } from '.'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Member tree that fetches data from the server + * @export + * @class MemberTreeServerDataSource + * @implements {MemberTreeDataSource} + */ +export class MemberTreeServerDataSource implements MemberTreeDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of MemberTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof MemberTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof MemberTreeServerDataSource + */ + async getRootItems() { + const response = await fetch('/umbraco/management/api/v1/tree/member/root'); + const data = await response.json(); + + return { data, error: undefined }; + //return tryExecuteAndNotify(this.#host, MemberResource.getTreeMemberRoot({})); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof MemberTreeServerDataSource + */ + async getItems(keys: Array) { + const response = await fetch('/umbraco/management/api/v1/tree/member/item'); + const data = await response.json(); + + return { data, error: undefined }; + + // if (keys) { + // const error: ProblemDetails = { title: 'Keys are missing' }; + // return { error }; + // } + + // return tryExecuteAndNotify( + // this.#host, + // MemberResource.getTreeMemberItem({ + // key: keys, + // }) + // ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts index d049dadb75..743b6f59e3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../member.tree.store'; +import { UmbMemberTreeRepository } from './data/member.tree.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { @@ -7,7 +7,7 @@ const tree: ManifestTree = { name: 'Members Tree', weight: 10, meta: { - storeAlias: UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString(), + repository: UmbMemberTreeRepository }, }; From 0aea6192d6ff0f238f05030c21e4308e32176fe4 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 9 Feb 2023 14:50:25 +0100 Subject: [PATCH 9/9] fix typescript errors --- .../tree/actions/action-member-group-delete.element.ts | 5 ++++- .../views/info/workspace-view-member-group-info.stories.ts | 6 +++--- .../members/tree/actions/action-member-delete.element.ts | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts index 9c4304f05b..f4eb25bcd0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/actions/action-member-group-delete.element.ts @@ -3,7 +3,7 @@ import { css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; -import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../../member-group.tree.store'; +import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from '../data/member-group.tree.store'; @customElement('umb-tree-action-member-group-delete') export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemActionElement { @@ -34,6 +34,9 @@ export default class UmbTreeActionMemberGroupDeleteElement extends UmbTreeItemAc modalHandler?.onClose().then(({ confirmed }: any) => { if (confirmed && this._treeContextMenuService && this._memberGroupTreeStore && this._activeTreeItem) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + /* @ts-ignore */ + // TODO: ignoring this error for now, because we will change this when entity actions are merged this._memberGroupTreeStore?.delete([this._activeTreeItem.key]); this._treeContextMenuService.close(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts index e2b88ed852..e95f947da8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/views/info/workspace-view-member-group-info.stories.ts @@ -6,7 +6,7 @@ import { html } from 'lit-html'; //import { data } from '../../../../../core/mocks/data/data-type.data'; //import { UmbDataTypeContext } from '../../data-type.context'; -import type { UmbWorkspaceViewDataTypeInfoElement } from './workspace-view-member-group-info.element'; +import type { UmbWorkspaceViewMemberGroupInfoElement } from './workspace-view-member-group-info.element'; export default { title: 'Workspaces/Data Type/Views/Info', @@ -18,10 +18,10 @@ export default { /*html` ${story()} `,*/ - } + }, ], } as Meta; -export const AAAOverview: Story = () => +export const AAAOverview: Story = () => html` `; AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts index 97b73c2bf8..ab85f6576a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-delete.element.ts @@ -34,6 +34,9 @@ export default class UmbTreeActionMemberDeleteElement extends UmbTreeItemActionE modalHandler?.onClose().then(({ confirmed }: any) => { if (confirmed && this._treeContextMenuService && this._memberTreeStore && this._activeTreeItem) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + /* @ts-ignore */ + // TODO: ignoring this error for now, because we will change this when entity actions are merged this._memberTreeStore?.delete([this._activeTreeItem.key]); this._treeContextMenuService.close(); }