diff --git a/src/Umbraco.Web.UI.Client/libs/models/index.ts b/src/Umbraco.Web.UI.Client/libs/models/index.ts index 85cb4f5486..b74476c065 100644 --- a/src/Umbraco.Web.UI.Client/libs/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/models/index.ts @@ -138,6 +138,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 b79cb4e537..af395aef34 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -25,7 +25,10 @@ import { UmbMediaDetailStore } from './media/media/repository/media.detail.store import { UmbMediaTreeStore } from './media/media/repository/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/tree/data/member-group.tree.store'; +import { UmbMemberDetailStore } from './members/members/member.detail.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'; @@ -93,7 +96,10 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbMemberTypeDetailStore(this); new UmbMemberTypeTreeStore(this); new UmbUserGroupStore(this); - new UmbMemberGroupStore(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 new file mode 100644 index 0000000000..bbf4f61c2d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.detail.store.ts @@ -0,0 +1,59 @@ +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, createObservablePart } from '@umbraco-cms/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; + +export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupDetailStore'); + +/** + * @export + * @class UmbMemberGroupDetailStore + * @extends {UmbStoreBase} + * @description - Detail Data Store for Member Groups + */ +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 + * @return {*} {(Observable)} + * @memberof UmbMemberGroupDetailStore + */ + 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 + ); + } + + 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/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..f4eb25bcd0 --- /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 { 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 { + static styles = [UUITextStyles, css``]; + + private _modalService?: UmbModalService; + private _memberGroupTreeStore?: UmbMemberGroupTreeStore; + + connectedCallback(): void { + super.connectedCallback(); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (modalService) => { + this._modalService = modalService; + }); + + this.consumeContext(UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN, (memberGroupTreeStore) => { + this._memberGroupTreeStore = memberGroupTreeStore; + }); + } + + 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._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(); + } + }); + } + + 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/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 8f65257d9a..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_STORE_CONTEXT_TOKEN } from '../member-group.details.store'; +import { UmbMemberGroupTreeRepository } from './data/member-group.tree.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.MemberGroups'; @@ -7,11 +7,25 @@ const tree: ManifestTree = { type: 'tree', alias: treeAlias, name: 'Member Groups Tree', + weight: 100, meta: { - storeAlias: UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString(), + repository: UmbMemberGroupTreeRepository }, }; -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..5ea4fc3f43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -0,0 +1,33 @@ +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'; + +export class UmbWorkspaceMemberGroupContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ + #manager = new UmbEntityWorkspaceManager(this._host, 'memberGroup', UMB_MEMBER_GROUP_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; +} 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..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,9 +1,18 @@ +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, state } from 'lit/decorators.js'; +import { distinctUntilChanged } from 'rxjs'; +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 LitElement { +export class UmbMemberGroupWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { static styles = [ UUITextStyles, css` @@ -12,15 +21,53 @@ 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 _workspaceContext: UmbWorkspaceMemberGroupContext = new UmbWorkspaceMemberGroupContext(this); - @property() - id!: string; + public load(entityKey: string) { + this._workspaceContext.load(entityKey); + } + + public create(parentKey: string | null) { + this._workspaceContext.create(parentKey); + } + + @state() + private _memberGroupName = ''; + + constructor() { + super(); + + 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.setName(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..cc2053fd03 --- /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 { UmbWorkspaceMemberGroupContext } from '../../member-group-workspace.context'; +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@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._observeMemberGroup(); + }); + } + + private _observeMemberGroup() { + 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..e95f947da8 --- /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 { UmbWorkspaceViewMemberGroupInfoElement } 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/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/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..ab85f6576a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/tree/actions/action-member-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 { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from '../data/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) { + // 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(); + } + }); + } + + 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/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 c3f4d051af..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,14 +1,29 @@ +import { UmbMemberTreeRepository } from './data/member.tree.repository'; 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', + repository: UmbMemberTreeRepository }, }; -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.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; +} 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/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} 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 bbaa846a72..f677db74d5 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'; @@ -40,6 +41,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 907ce96d68..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 { UmbEntityData } from './entity.data'; import { createEntityTreeItem } from './utils'; -import { EntityTreeItem, PagedEntityTreeItem } from '@umbraco-cms/backend-api'; 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)); + }), +];