From e7abf9e2fceecf20222a20379faa46e7b77478ae Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Fri, 27 Jan 2023 14:53:37 +1000 Subject: [PATCH 001/174] 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 002/174] 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 003/174] 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 0b9daee31231d02df5260db8228f86306df7a547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Tue, 31 Jan 2023 10:58:30 +0100 Subject: [PATCH 004/174] add available languages --- .../settings/languages/language.store.ts | 16 +++++++++ .../language/language-workspace.context.ts | 6 ++++ .../workspace-view-language-edit.element.ts | 35 +++++++++++-------- .../src/core/mocks/data/languages.data.ts | 20 +++++++++++ .../core/mocks/domains/language.handlers.ts | 5 +++ 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts index 9ca2ef3efb..e0b73e9525 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts @@ -5,6 +5,7 @@ import { UmbContextToken } from '@umbraco-cms/context-api'; import { UmbStoreBase } from '@umbraco-cms/store'; import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { umbracoPath } from '@umbraco-cms/utils'; export type UmbLanguageStoreItemType = Language; export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken('umbLanguageStore'); @@ -17,6 +18,9 @@ export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.isoCode); + #availableLanguages = new ArrayState([], (x) => x.isoCode); + + public readonly availableLanguages = this.#availableLanguages.asObservable(); constructor(host: UmbControllerHostInterface) { super(host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN.toString()); @@ -40,6 +44,18 @@ export class UmbLanguageStore extends UmbStoreBase { return this.#data; } + getAvailable() { + fetch(umbracoPath('/languages').toString()) + .then((res) => res.json()) + .then((data) => { + console.log('data', data); + + this.#availableLanguages.append(data); + }); + + return this.availableLanguages; + } + async save(language: UmbLanguageStoreItemType): Promise { if (language.isoCode) { const { data: updatedLanguage } = await tryExecuteAndNotify( diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts index ca6945bcbe..ace13410ee 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts @@ -49,6 +49,12 @@ export class UmbWorkspaceLanguageContext { public getData() { return this._data.getValue(); } + + public getAvailableLanguages() { + //TODO: Don't use !, however this will be changed with the introduction of repositories. + return this._store!.getAvailable(); + } + public update(data: Partial) { this._data.next({ ...this.getData(), ...data }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index 7d7186fa87..a59a338b0d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -45,9 +45,15 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { @state() private _languages: UmbLanguageStoreItemType[] = []; + @state() + private _availableLanguages: UmbLanguageStoreItemType[] = []; + @state() private _search = ''; + @state() + private _startedAsDefault: boolean | null = null; + private _languageWorkspaceContext?: UmbWorkspaceLanguageContext; constructor() { @@ -58,8 +64,16 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { if (!this._languageWorkspaceContext) return; - this._languageWorkspaceContext.data.subscribe((language) => { + this.observe(this._languageWorkspaceContext.data, (language) => { this.language = language; + + if (this._startedAsDefault === null) { + this._startedAsDefault = language.isDefault ?? false; + console.log('first', language); + } + }); + this.observe(this._languageWorkspaceContext.getAvailableLanguages(), (languages) => { + this._availableLanguages = languages; }); }); @@ -116,7 +130,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { } private get _filteredLanguages() { - return this._languages.filter((language) => { + return this._availableLanguages.filter((language) => { return language.name?.toLowerCase().includes(this._search.toLowerCase()); }); } @@ -131,19 +145,12 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { return this._fallbackLanguages.find((language) => language.isoCode === this.language?.fallbackIsoCode); } - private get _nonEditedLanguage() { - return this._languages.find((language) => language.isoCode === this.language?.isoCode); + private get _fromAvailableLanguages() { + return this._availableLanguages.find((language) => language.isoCode === this.language?.isoCode); } private _renderDefaultLanguageWarning() { - let originalIsDefault = false; - - if (this.language?.isoCode) { - originalIsDefault = - this._languages.find((language) => language.isoCode === this.language?.isoCode)?.isDefault ?? false; - } - - if (originalIsDefault === this.language?.isDefault) return nothing; + if (this._startedAsDefault || this.language?.isDefault !== true) return nothing; return html`
Switching default language may result in default content missing. @@ -158,7 +165,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { @@ -181,7 +188,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts index 09a4e71887..e3f5310d0b 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts @@ -16,6 +16,10 @@ class UmbLanguagesData extends UmbData { return this.data.find((item) => item.isoCode === key); } + getAvailable() { + return MockAvailable; + } + save(saveItems: Array) { saveItems.forEach((saveItem) => { const foundIndex = this.data.findIndex((item) => item.isoCode === saveItem.isoCode); @@ -86,6 +90,22 @@ export const MockData: Array = [ isMandatory: false, fallbackIsoCode: 'en', }, +]; + +export const MockAvailable: Array = [ + { + name: 'English', + isoCode: 'en', + isDefault: true, + isMandatory: true, + }, + { + name: 'Danish', + isoCode: 'da', + isDefault: false, + isMandatory: false, + fallbackIsoCode: 'en', + }, { name: 'German', isoCode: 'de', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts index 04558b6547..0640d11735 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts @@ -22,6 +22,11 @@ export const handlers = [ return res(ctx.status(200), ctx.json(response)); }), + rest.get(umbracoPath('/languages'), (req, res, ctx) => { + const items = umbLanguagesData.getAvailable(); + return res(ctx.status(200), ctx.json(items)); + }), + rest.get(umbracoPath('/language/:key'), (req, res, ctx) => { const key = req.params.key as string; From 0f9049c6a3da9596e91161531d8630756cbd1974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Tue, 31 Jan 2023 11:11:24 +0100 Subject: [PATCH 005/174] WIP Filter languages --- .../views/edit/workspace-view-language-edit.element.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index a59a338b0d..f5d5477c25 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -11,6 +11,7 @@ import { UMB_LANGUAGE_STORE_CONTEXT_TOKEN, } from '../../../../language.store'; import { UmbLitElement } from '@umbraco-cms/element'; +import { Language } from '@umbraco-cms/backend-api'; @customElement('umb-workspace-view-language-edit') export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { @@ -129,10 +130,11 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { } } - private get _filteredLanguages() { - return this._availableLanguages.filter((language) => { - return language.name?.toLowerCase().includes(this._search.toLowerCase()); - }); + private get _filteredLanguages(): Array { + // Filter out languages already in use, except the current language. + return this._availableLanguages.filter( + (language) => !this._languages.some((x) => x.isoCode === language.isoCode && x.isoCode !== this.language?.isoCode) + ); } private get _fallbackLanguages() { From 0930c96fbc9841fb7e32f7baba6c1cf1b7ab5643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 00:47:45 +0100 Subject: [PATCH 006/174] Filter out already used languages from language selector --- .../settings/languages/language.store.ts | 2 -- .../workspace-view-language-edit.element.ts | 21 +++++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts index e0b73e9525..9308af997c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts @@ -48,8 +48,6 @@ export class UmbLanguageStore extends UmbStoreBase { fetch(umbracoPath('/languages').toString()) .then((res) => res.json()) .then((data) => { - console.log('data', data); - this.#availableLanguages.append(data); }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index f5d5477c25..f74f6cc462 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -53,7 +53,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { private _search = ''; @state() - private _startedAsDefault: boolean | null = null; + private _startData: Language | null = null; private _languageWorkspaceContext?: UmbWorkspaceLanguageContext; @@ -68,9 +68,8 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { this.observe(this._languageWorkspaceContext.data, (language) => { this.language = language; - if (this._startedAsDefault === null) { - this._startedAsDefault = language.isDefault ?? false; - console.log('first', language); + if (this._startData === null) { + this._startData = language; } }); this.observe(this._languageWorkspaceContext.getAvailableLanguages(), (languages) => { @@ -131,10 +130,14 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { } private get _filteredLanguages(): Array { - // Filter out languages already in use, except the current language. - return this._availableLanguages.filter( - (language) => !this._languages.some((x) => x.isoCode === language.isoCode && x.isoCode !== this.language?.isoCode) + const onlyNewLanguages = this._availableLanguages.filter( + (language) => + !this._languages.some((x) => x.isoCode === language.isoCode && x.isoCode !== this._startData?.isoCode) ); + + return onlyNewLanguages.filter((language) => { + return language.name?.toLowerCase().includes(this._search.toLowerCase()); + }); } private get _fallbackLanguages() { @@ -152,7 +155,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { } private _renderDefaultLanguageWarning() { - if (this._startedAsDefault || this.language?.isDefault !== true) return nothing; + if (this._startData?.isDefault || this.language?.isDefault !== true) return nothing; return html`
Switching default language may result in default content missing. @@ -190,7 +193,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
From e6aadc7fe3d95e4d8beb124fcd25a6bff2ca34ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 00:49:01 +0100 Subject: [PATCH 007/174] Fix language name autocomplete --- .../language/views/edit/workspace-view-language-edit.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index f74f6cc462..1549d873c9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -93,7 +93,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { // If the language name is not set, we set it to the name of the selected language. if (!this.language?.name) { - const language = this._languages.find((language) => language.isoCode === target.value.toString()); + const language = this._availableLanguages.find((language) => language.isoCode === target.value.toString()); if (language) { this._languageWorkspaceContext?.update({ name: language.name }); } From 6827dc97f5139d35e6ca18d641f23b99b790d173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 00:58:12 +0100 Subject: [PATCH 008/174] Creating a new default language will correctly remove default from other languages --- .../src/core/mocks/data/languages.data.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts index e3f5310d0b..a5fc5ee05e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts @@ -28,6 +28,14 @@ class UmbLanguagesData extends UmbData { this.data[foundIndex] = saveItem; this.updateData(saveItem); } else { + // Set all other languages to not default + if (saveItem.isDefault) { + this.data.forEach((item) => { + if (saveItem !== item) { + item.isDefault = false; + } + }); + } this.data.push(saveItem); } }); @@ -54,6 +62,7 @@ class UmbLanguagesData extends UmbData { const itemKeys = Object.keys(item); const newItem = {}; + // Set all other languages to not default if (updateItem.isDefault) { this.data.forEach((item) => { if (updateItem !== item) { From 7f12a8634c1494cb3127f8ce89f05ff94626397a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 01:14:14 +0100 Subject: [PATCH 009/174] add culture warning --- .../workspace-view-language-edit.element.ts | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index 1549d873c9..3d093baf47 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -29,13 +29,22 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { border: none; border-bottom: 1px solid var(--uui-color-divider); } + #culture-warning, + #default-language-warning { + padding: var(--uui-size-space-4) var(--uui-size-space-5); + border: 1px solid; + margin-top: var(--uui-size-space-4); + border-radius: var(--uui-border-radius); + } + #culture-warning { + background-color: var(--uui-color-danger); + color: var(--uui-color-danger-contrast); + border-color: var(--uui-color-danger-standalone); + } #default-language-warning { background-color: var(--uui-color-warning); color: var(--uui-color-warning-contrast); - padding: var(--uui-size-space-4) var(--uui-size-space-5); - border: 1px solid var(--uui-color-warning-standalone); - margin-top: var(--uui-size-space-4); - border-radius: var(--uui-border-radius); + border-color: var(--uui-color-warning-standalone); } `, ]; @@ -154,6 +163,15 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { return this._availableLanguages.find((language) => language.isoCode === this.language?.isoCode); } + private _renderCultureWarning() { + if (this._startData?.isoCode === this.language?.isoCode) return nothing; + + return html`
+ Changing the culture for a language may be an expensive operation and will result in the content cache and indexes + being rebuilt. +
`; + } + private _renderDefaultLanguageWarning() { if (this._startData?.isDefault || this.language?.isDefault !== true) return nothing; @@ -168,24 +186,26 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { return html` - - - ${repeat( - this._filteredLanguages, - (language) => language.isoCode, - (language) => - html` - ${language.name} - ` - )} - - +
+ + + ${repeat( + this._filteredLanguages, + (language) => language.isoCode, + (language) => + html` + ${language.name} + ` + )} + + + ${this._renderCultureWarning()} +
${this.language.isoCode}
@@ -201,6 +221,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
An Umbraco site can only have one default language set.
+ ${this._renderDefaultLanguageWarning()}
@@ -208,7 +229,6 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
Properties on this language have to be filled out before the node can be published.
- ${this._renderDefaultLanguageWarning()}
Date: Wed, 1 Feb 2023 01:20:04 +0100 Subject: [PATCH 010/174] cleanup --- ...root-table-delete-column-layout.element.ts | 18 ++--- .../language-root-workspace.element.ts | 14 ++-- .../language/language-workspace.context.ts | 32 ++++----- .../language/language-workspace.element.ts | 14 ++-- .../workspace-view-language-edit.element.ts | 66 +++++++++---------- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts index 927146b774..ac3b22a1f4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts @@ -12,25 +12,25 @@ export class UmbLanguageRootTableDeleteColumnLayoutElement extends UmbLitElement @property({ attribute: false }) value!: UmbLanguageStoreItemType; - private _languageStore?: UmbLanguageStore; - private _modalService?: UmbModalService; + #languageStore?: UmbLanguageStore; + #modalService?: UmbModalService; constructor() { super(); this.consumeContext(UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { - this._languageStore = instance; + this.#languageStore = instance; }); this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { - this._modalService = instance; + this.#modalService = instance; }); } - private _handleDelete(event: MouseEvent) { + #handleDelete(event: MouseEvent) { event.stopImmediatePropagation(); - if (!this._languageStore) return; + if (!this.#languageStore) return; - const modalHandler = this._modalService?.confirm({ + const modalHandler = this.#modalService?.confirm({ headline: 'Delete language', content: html`
{ if (confirmed) { - this._languageStore?.delete([this.value.isoCode!]); + this.#languageStore?.delete([this.value.isoCode!]); } }); } @@ -54,7 +54,7 @@ export class UmbLanguageRootTableDeleteColumnLayoutElement extends UmbLitElement if (this.value.isDefault) return nothing; return html` = []; - private _languageStore?: UmbLanguageStore; + #languageStore?: UmbLanguageStore; constructor() { super(); this.consumeContext(UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { - this._languageStore = instance; - this._observeLanguages(); + this.#languageStore = instance; + this.#observeLanguages(); }); } @@ -89,13 +89,13 @@ export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements Um // Not relevant for this workspace } - private _observeLanguages() { - this._languageStore?.getAll().subscribe((languages) => { - this._createTableItems(languages); + #observeLanguages() { + this.#languageStore?.getAll().subscribe((languages) => { + this.#createTableItems(languages); }); } - private _createTableItems(languages: Array) { + #createTableItems(languages: Array) { this._tableItems = languages.map((language) => { return { key: language.isoCode ?? '', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts index ace13410ee..bfef549d6a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts @@ -13,58 +13,58 @@ const DefaultLanguageData: UmbLanguageStoreItemType = { export class UmbWorkspaceLanguageContext { public host: UmbControllerHostInterface; - private _entityKey: string | null; + #entityKey: string | null; - private _data; + #data; public readonly data; - private _store: UmbLanguageStore | null = null; + #store: UmbLanguageStore | null = null; protected _storeObserver?: UmbObserverController; constructor(host: UmbControllerHostInterface, entityKey: string | null) { this.host = host; - this._entityKey = entityKey; + this.#entityKey = entityKey; - this._data = new ObjectState(DefaultLanguageData); - this.data = this._data.asObservable(); + this.#data = new ObjectState(DefaultLanguageData); + this.data = this.#data.asObservable(); new UmbContextConsumerController(host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (_instance: UmbLanguageStore) => { - this._store = _instance; - this._observeStore(); + this.#store = _instance; + this.#observeStore(); }); } - private _observeStore(): void { - if (!this._store || this._entityKey === null) { + #observeStore(): void { + if (!this.#store || this.#entityKey === null) { return; } this._storeObserver?.destroy(); - this._storeObserver = new UmbObserverController(this.host, this._store.getByIsoCode(this._entityKey), (content) => { + this._storeObserver = new UmbObserverController(this.host, this.#store.getByIsoCode(this.#entityKey), (content) => { if (!content) return; // TODO: Handle nicely if there is no content data. this.update(content); }); } public getData() { - return this._data.getValue(); + return this.#data.getValue(); } public getAvailableLanguages() { //TODO: Don't use !, however this will be changed with the introduction of repositories. - return this._store!.getAvailable(); + return this.#store!.getAvailable(); } public update(data: Partial) { - this._data.next({ ...this.getData(), ...data }); + this.#data.next({ ...this.getData(), ...data }); } public save(): Promise { - if (!this._store) { + if (!this.#store) { // TODO: more beautiful error: console.error('Could not save cause workspace context has no store.'); return Promise.resolve(); } - return this._store.save(this.getData()); + return this.#store.save(this.getData()); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts index 20a4af6d39..27158fa1cb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts @@ -28,7 +28,7 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor @property() language?: UmbLanguageStoreItemType; - private _languageWorkspaceContext?: UmbWorkspaceLanguageContext; + #languageWorkspaceContext?: UmbWorkspaceLanguageContext; load(key: string): void { this.provideLanguageWorkspaceContext(key); @@ -39,19 +39,19 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor } public provideLanguageWorkspaceContext(entityKey: string | null) { - this._languageWorkspaceContext = new UmbWorkspaceLanguageContext(this, entityKey); - this.provideContext('umbWorkspaceContext', this._languageWorkspaceContext); - this._languageWorkspaceContext.data.subscribe((language) => { + this.#languageWorkspaceContext = new UmbWorkspaceLanguageContext(this, entityKey); + this.provideContext('umbWorkspaceContext', this.#languageWorkspaceContext); + this.#languageWorkspaceContext.data.subscribe((language) => { this.language = language; }); } - private _handleInput(event: UUIInputEvent) { + #handleInput(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { - this._languageWorkspaceContext?.update({ name: target.value }); + this.#languageWorkspaceContext?.update({ name: target.value }); } } } @@ -67,7 +67,7 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor - +
`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index 3d093baf47..eaf73dc108 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -64,24 +64,24 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { @state() private _startData: Language | null = null; - private _languageWorkspaceContext?: UmbWorkspaceLanguageContext; + #languageWorkspaceContext?: UmbWorkspaceLanguageContext; constructor() { super(); this.consumeContext('umbWorkspaceContext', (instance) => { - this._languageWorkspaceContext = instance; + this.#languageWorkspaceContext = instance; - if (!this._languageWorkspaceContext) return; + if (!this.#languageWorkspaceContext) return; - this.observe(this._languageWorkspaceContext.data, (language) => { + this.observe(this.#languageWorkspaceContext.data, (language) => { this.language = language; if (this._startData === null) { this._startData = language; } }); - this.observe(this._languageWorkspaceContext.getAvailableLanguages(), (languages) => { + this.observe(this.#languageWorkspaceContext.getAvailableLanguages(), (languages) => { this._availableLanguages = languages; }); }); @@ -95,50 +95,50 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { }); } - private _handleLanguageChange(event: Event) { + #handleLanguageChange(event: Event) { if (event instanceof UUIComboboxEvent) { const target = event.composedPath()[0] as UUIComboboxElement; - this._languageWorkspaceContext?.update({ isoCode: target.value.toString() }); + this.#languageWorkspaceContext?.update({ isoCode: target.value.toString() }); // If the language name is not set, we set it to the name of the selected language. if (!this.language?.name) { const language = this._availableLanguages.find((language) => language.isoCode === target.value.toString()); if (language) { - this._languageWorkspaceContext?.update({ name: language.name }); + this.#languageWorkspaceContext?.update({ name: language.name }); } } } } - private _handleSearchChange(event: Event) { + #handleSearchChange(event: Event) { const target = event.composedPath()[0] as UUIComboboxElement; this._search = target.search; } - private _handleDefaultChange(event: UUIBooleanInputEvent) { + #handleDefaultChange(event: UUIBooleanInputEvent) { if (event instanceof UUIBooleanInputEvent) { const target = event.composedPath()[0] as UUIToggleElement; - this._languageWorkspaceContext?.update({ isDefault: target.checked }); + this.#languageWorkspaceContext?.update({ isDefault: target.checked }); } } - private _handleMandatoryChange(event: UUIBooleanInputEvent) { + #handleMandatoryChange(event: UUIBooleanInputEvent) { if (event instanceof UUIBooleanInputEvent) { const target = event.composedPath()[0] as UUIToggleElement; - this._languageWorkspaceContext?.update({ isMandatory: target.checked }); + this.#languageWorkspaceContext?.update({ isMandatory: target.checked }); } } - private _handleFallbackChange(event: UUIComboboxEvent) { + #handleFallbackChange(event: UUIComboboxEvent) { if (event instanceof UUIComboboxEvent) { const target = event.composedPath()[0] as UUIComboboxElement; - this._languageWorkspaceContext?.update({ fallbackIsoCode: target.value.toString() }); + this.#languageWorkspaceContext?.update({ fallbackIsoCode: target.value.toString() }); } } - private get _filteredLanguages(): Array { + get #filteredLanguages(): Array { const onlyNewLanguages = this._availableLanguages.filter( (language) => !this._languages.some((x) => x.isoCode === language.isoCode && x.isoCode !== this._startData?.isoCode) @@ -149,21 +149,21 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { }); } - private get _fallbackLanguages() { + get #fallbackLanguages() { return this._languages.filter((language) => { return language.isoCode !== this.language?.isoCode; }); } - private get _fallbackLanguage() { - return this._fallbackLanguages.find((language) => language.isoCode === this.language?.fallbackIsoCode); + get #fallbackLanguage() { + return this.#fallbackLanguages.find((language) => language.isoCode === this.language?.fallbackIsoCode); } - private get _fromAvailableLanguages() { + get #fromAvailableLanguages() { return this._availableLanguages.find((language) => language.isoCode === this.language?.isoCode); } - private _renderCultureWarning() { + #renderCultureWarning() { if (this._startData?.isoCode === this.language?.isoCode) return nothing; return html`
@@ -172,7 +172,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
`; } - private _renderDefaultLanguageWarning() { + #renderDefaultLanguageWarning() { if (this._startData?.isDefault || this.language?.isDefault !== true) return nothing; return html`
@@ -188,12 +188,12 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement {
+ value=${ifDefined(this.#fromAvailableLanguages?.isoCode)} + @change=${this.#handleLanguageChange} + @search=${this.#handleSearchChange}> ${repeat( - this._filteredLanguages, + this.#filteredLanguages, (language) => language.isoCode, (language) => html` @@ -204,7 +204,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { )} - ${this._renderCultureWarning()} + ${this.#renderCultureWarning()}
@@ -215,15 +215,15 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { + @change=${this.#handleDefaultChange}>
Default language
An Umbraco site can only have one default language set.
- ${this._renderDefaultLanguageWarning()} + ${this.#renderDefaultLanguageWarning()}
- +
Mandatory language
Properties on this language have to be filled out before the node can be published.
@@ -236,11 +236,11 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { description="To allow multi-lingual content to fall back to another language if not present in the requested language, select it here."> + value=${ifDefined(this.#fallbackLanguage?.isoCode)} + @change=${this.#handleFallbackChange}> ${repeat( - this._fallbackLanguages, + this.#fallbackLanguages, (language) => language.isoCode, (language) => html` From 0c478d0665038a67ef1df1819a66f378d6090c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 01:30:42 +0100 Subject: [PATCH 011/174] don't show culture warning when creating new language --- .../language/views/edit/workspace-view-language-edit.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index eaf73dc108..b87993bcd9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -164,7 +164,7 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { } #renderCultureWarning() { - if (this._startData?.isoCode === this.language?.isoCode) return nothing; + if (!this._startData?.isoCode || this._startData?.isoCode === this.language?.isoCode) return nothing; return html`
Changing the culture for a language may be an expensive operation and will result in the content cache and indexes From c1aecc810ca2fd813f8da0510f678e90adc3b35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 1 Feb 2023 01:58:37 +0100 Subject: [PATCH 012/174] Better UX with the language select combobox --- .../workspace-view-language-edit.element.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts index b87993bcd9..98aaae863e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts @@ -98,14 +98,28 @@ export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { #handleLanguageChange(event: Event) { if (event instanceof UUIComboboxEvent) { const target = event.composedPath()[0] as UUIComboboxElement; - this.#languageWorkspaceContext?.update({ isoCode: target.value.toString() }); + const isoCode = target.value.toString(); - // If the language name is not set, we set it to the name of the selected language. - if (!this.language?.name) { - const language = this._availableLanguages.find((language) => language.isoCode === target.value.toString()); - if (language) { - this.#languageWorkspaceContext?.update({ name: language.name }); + if (isoCode) { + this.#languageWorkspaceContext?.update({ isoCode: target.value.toString() }); + + // If the language name is not set, we set it to the name of the selected language. + if (!this.language?.name) { + const language = this._availableLanguages.find((language) => language.isoCode === target.value.toString()); + if (language) { + this.#languageWorkspaceContext?.update({ name: language.name }); + } } + } else { + // If the isoCode is empty, we reset the value to the original value. + // Provides a way better UX + //TODO: Maybe the combobox should implement something similar? + const resetFunction = () => { + target.value = this.language?.isoCode ?? ''; + }; + + target.addEventListener('close', resetFunction, { once: true }); + target.addEventListener('blur', resetFunction, { once: true }); } } } From a3425c9b4f01e5a8a996481c1f84707bb696f481 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 08:32:24 +0100 Subject: [PATCH 013/174] add entity action models --- .../extensions-registry/entity-action.models.ts | 13 +++++++++++++ .../libs/extensions-registry/models.ts | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts new file mode 100644 index 0000000000..e44ee7d6af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts @@ -0,0 +1,13 @@ +import type { ManifestElement } from './models'; + +export interface ManifestEntityAction extends ManifestElement { + type: 'entityAction'; + meta: MetaEntityAction; +} + +export interface MetaEntityAction { + icon: string; + label: string; + entityType: string; + api: any; // create interface +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts index 02856862df..995fb01def 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts @@ -19,6 +19,7 @@ import type { ManifestCollectionView } from './collection-view.models'; import type { ManifestHealthCheck } from './health-check.models'; import type { ManifestSidebarMenuItem } from './sidebar-menu-item.models'; import type { ManifestTheme } from './theme.models'; +import type { ManifestEntityAction } from './entity-action.models'; export * from './header-app.models'; export * from './section.models'; @@ -66,7 +67,8 @@ export type ManifestTypes = | ManifestCollectionView | ManifestHealthCheck | ManifestSidebarMenuItem - | ManifestTheme; + | ManifestTheme + | ManifestEntityAction; export type ManifestStandardTypes = ManifestTypes['type']; @@ -103,7 +105,6 @@ export interface MetaManifestWithView { icon: string; } - export interface ManifestElementWithElementName extends ManifestElement { elementName: string; } From 7ca7b77ceedcb3ee2e03d3b94a363aac0c6d6a46 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 08:32:57 +0100 Subject: [PATCH 014/174] register entity actions for documents --- .../documents/entity-actions/manifests.ts | 42 +++++++++++++++++++ .../documents/documents/manifests.ts | 8 +++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts new file mode 100644 index 0000000000..8c172251e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts @@ -0,0 +1,42 @@ +import { CreateDocumentEntityAction } from './document-create.entity-action'; +import { DeleteDocumentEntityAction } from './document-delete.entity-action'; +import { PublishDocumentEntityAction } from './document-publish.entity-action'; +import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.Document.Create', + name: 'Create Document Entity Action ', + meta: { + entityType: 'document', + icon: 'umb:add', + label: 'Create', + api: CreateDocumentEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.Document.Delete', + name: 'Delete Document Entity Action ', + meta: { + entityType: 'document', + icon: 'umb:trash', + label: 'Delete', + api: DeleteDocumentEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.Document.Publish', + name: 'Publish Document Entity Action ', + meta: { + entityType: 'document', + icon: 'umb:document', + label: 'Publish', + api: PublishDocumentEntityAction, + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts index a4edc8b4f1..84d347e156 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts @@ -1,5 +1,11 @@ import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; +import { manifests as entityActionManifests } from './entity-actions/manifests'; -export const manifests = [...sidebarMenuItemManifests, ...treeManifests, ...workspaceManifests]; +export const manifests = [ + ...sidebarMenuItemManifests, + ...treeManifests, + ...workspaceManifests, + ...entityActionManifests, +]; From e41f962420ea4223f0a028a9775be53f08b5f70f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 08:33:24 +0100 Subject: [PATCH 015/174] add api scaffolds --- .../entity-actions/document-create.entity-action.ts | 11 +++++++++++ .../entity-actions/document-delete.entity-action.ts | 11 +++++++++++ .../entity-actions/document-publish.entity-action.ts | 11 +++++++++++ 3 files changed, 33 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-create.entity-action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-publish.entity-action.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-create.entity-action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-create.entity-action.ts new file mode 100644 index 0000000000..162641eebe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-create.entity-action.ts @@ -0,0 +1,11 @@ +export class CreateDocumentEntityAction { + #host: any; + + constructor(host: any) { + this.#host = host; + } + + execute() { + alert('create'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts new file mode 100644 index 0000000000..889c49dc61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts @@ -0,0 +1,11 @@ +export class DeleteDocumentEntityAction { + #host: any; + + constructor(host: any) { + this.#host = host; + } + + execute() { + alert('delete'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-publish.entity-action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-publish.entity-action.ts new file mode 100644 index 0000000000..6f518aa0ef --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-publish.entity-action.ts @@ -0,0 +1,11 @@ +export class PublishDocumentEntityAction { + #host: any; + + constructor(host: any) { + this.#host = host; + } + + execute() { + alert('publish'); + } +} From 98556ce25cbc9aaa16bd09c53bf6f0631bb98216 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 08:34:17 +0100 Subject: [PATCH 016/174] render entity actions in workspace layout --- .../workspace/document-workspace.element.ts | 2 +- .../body-layout/body-layout.element.ts | 35 ++++++-- .../workspace/entity-action.element.ts | 43 ++++++++++ .../workspace-content.element.ts | 7 +- .../workspace-layout.element.ts | 84 +++++++++++++------ 5 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts index 55ddd6a39d..b7c12de7c1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts @@ -29,7 +29,7 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement implements UmbWor } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts index 92f2b23cfc..f165f804c0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts @@ -64,10 +64,6 @@ export class UmbBodyLayout extends LitElement { `, ]; - private hasNodes = (e: Event) => { - return (e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0; - }; - /** * Renders a headline in the header. * @public @@ -80,30 +76,51 @@ export class UmbBodyLayout extends LitElement { @state() private _headerSlotHasChildren = false; + @state() private _tabsSlotHasChildren = false; + @state() private _footerSlotHasChildren = false; + @state() private _actionsSlotHasChildren = false; + @state() + private _actionsMenuSlotHasChildren = false; + + #hasNodes = (e: Event) => { + return (e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0; + }; + render() { return html` @@ -113,14 +130,14 @@ export class UmbBodyLayout extends LitElement { { - this._footerSlotHasChildren = this.hasNodes(e); + this._footerSlotHasChildren = this.#hasNodes(e); }}> { - this._actionsSlotHasChildren = this.hasNodes(e); + this._actionsSlotHasChildren = this.#hasNodes(e); }}>
`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts new file mode 100644 index 0000000000..71e377339e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts @@ -0,0 +1,43 @@ +import { html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; + +@customElement('umb-entity-action') +class UmbEntityActionElement extends UmbLitElement { + private _manifest?: ManifestEntityAction; + @property({ type: Object, attribute: false }) + public get entityType() { + return this._manifest; + } + public set manifest(value: ManifestEntityAction | undefined) { + if (!value) return; + const oldValue = this._manifest; + this._manifest = value; + if (oldValue !== this._manifest) { + this.#api = new this._manifest.meta.api(this); + this.requestUpdate('manifest', oldValue); + } + } + + #api: any; + + #onClickLabel() { + this.#api.execute(); + } + + render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-action': UmbEntityActionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts index 0593b4ae60..49de100c77 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts @@ -15,7 +15,7 @@ import { UmbLitElement } from '@umbraco-cms/element'; * TODO: IMPORTANT TODO: Get rid of the content workspace. Instead we aim to get separate components that can be composed by each workspace. * Example. Document Workspace would use a Variant-component(variant component would talk directly to the workspace-context) * As well breadcrumbs etc. - * + * */ @customElement('umb-workspace-content') export class UmbWorkspaceContentElement extends UmbLitElement { @@ -43,9 +43,12 @@ export class UmbWorkspaceContentElement extends UmbLitElement { @property() alias!: string; + @property({ type: String, attribute: 'entity-type' }) + public entityType = ''; + render() { return html` - + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts index fc697a56c8..203edddfd7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts @@ -16,6 +16,9 @@ import type { import '../../body-layout/body-layout.element'; import '../../extension-slot/extension-slot.element'; import { UmbLitElement } from '@umbraco-cms/element'; +import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; + +import '../entity-action.element'; /** * @element umb-workspace-layout @@ -61,6 +64,20 @@ export class UmbWorkspaceLayout extends UmbLitElement { `, ]; + private _entityType = ''; + @property({ type: String, attribute: 'entity-type' }) + public get entityType() { + return this._entityType; + } + public set entityType(value) { + const oldValue = this._entityType; + this._entityType = value; + if (oldValue !== this._entityType) { + this.#observeEntityActions(); + this.requestUpdate('entityType', oldValue); + } + } + @property() public headline = ''; @@ -97,6 +114,15 @@ export class UmbWorkspaceLayout extends UmbLitElement { @state() private _activePath?: string; + @state() + private _entityActions?: Array; + + #observeEntityActions() { + this.observe(umbExtensionsRegistry.extensionsOfType('entityAction'), (actions) => { + this._entityActions = actions; + }); + } + private _observeWorkspaceViews() { this.observe( umbExtensionsRegistry @@ -142,7 +168,34 @@ export class UmbWorkspaceLayout extends UmbLitElement { } } - private _renderTabs() { + render() { + return html` + + + ${this.#renderTabs()} ${this.#renderActionsMenu()} + + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}> + + + + + extension.meta.workspaces.includes(this.alias)}> + + + `; + } + + #renderTabs() { return html` ${this._workspaceViews.length > 0 ? html` @@ -166,31 +219,10 @@ export class UmbWorkspaceLayout extends UmbLitElement { `; } - render() { - return html` - - - ${this._renderTabs()} - - { - this._routerPath = event.target.absoluteRouterPath; - }} - @change=${(event: UmbRouterSlotChangeEvent) => { - this._activePath = event.target.localActiveViewPath; - }}> - - - - - extension.meta.workspaces.includes(this.alias)}> - - - `; + #renderActionsMenu() { + return html`
+ ${this._entityActions?.map((manifest) => html``)} +
`; } } From 04934c5132c963db2f1a7c724dfd968fc1340328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 2 Feb 2023 09:45:57 +0100 Subject: [PATCH 017/174] remove unused prop --- .../src/backoffice/shared/components/tree/tree.element.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts index 7fba991109..40873a3c23 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts @@ -64,8 +64,6 @@ export class UmbTreeElement extends UmbLitElement { private _treeContext?: UmbTreeContextBase; private _store?: UmbTreeStore; - #treeRepository?: any; // TODO: make interface - protected firstUpdated(): void { this._observeTree(); } From 26880a637f45c17660d9baf6d212f0847d525c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 2 Feb 2023 09:47:06 +0100 Subject: [PATCH 018/174] comments --- .../src/backoffice/shared/components/tree/tree.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts index 40873a3c23..a963ec309b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts @@ -137,6 +137,7 @@ export class UmbTreeElement extends UmbLitElement { }); } + //TODO: remove when repositories are fully implemented: private _observeStoreTreeRoot() { if (!this._store?.getTreeRoot) return; @@ -150,7 +151,6 @@ export class UmbTreeElement extends UmbLitElement { } render() { - // TODO: Fix Type Mismatch ` as Entity` in this template: return html` ${repeat( this._items, From 50302a26b48d689d0cbf57b19d69f71824991a8a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 10:48:53 +0100 Subject: [PATCH 019/174] render icon on action --- .../components/workspace/entity-action.element.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts index 71e377339e..16d72680a9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/entity-action.element.ts @@ -1,4 +1,4 @@ -import { html } from 'lit'; +import { html, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -23,15 +23,17 @@ class UmbEntityActionElement extends UmbLitElement { #api: any; - #onClickLabel() { - this.#api.execute(); + async #onClickLabel() { + await this.#api.execute(); } render() { return html` - + + ${this._manifest?.meta.icon + ? html`` + : nothing} + `; } } From b6108a097f704533cfe656771cdb0138d91c90b2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 10:49:50 +0100 Subject: [PATCH 020/174] rename delete to trash --- .../document-delete.entity-action.ts | 11 ----- .../document-trash.entity-action.ts | 45 +++++++++++++++++++ .../documents/entity-actions/manifests.ts | 10 ++--- 3 files changed, 50 insertions(+), 16 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-trash.entity-action.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts deleted file mode 100644 index 889c49dc61..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-delete.entity-action.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class DeleteDocumentEntityAction { - #host: any; - - constructor(host: any) { - this.#host = host; - } - - execute() { - alert('delete'); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-trash.entity-action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-trash.entity-action.ts new file mode 100644 index 0000000000..7bada10a2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/document-trash.entity-action.ts @@ -0,0 +1,45 @@ +import { UmbTemplateTreeRepository } from '../../../templating/templates/tree/data/template.tree.repository'; +import { UmbTemplateDetailRepository } from '../../../templating/templates/workspace/data/template.detail.repository'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; + +export class TrashDocumentEntityAction { + #host: UmbControllerHostInterface; + #key: string; + #modalService?: UmbModalService; + #documentDetailRepo: UmbTemplateDetailRepository; + #documentTreeRepo: UmbTemplateTreeRepository; + + constructor(host: UmbControllerHostInterface, key: string) { + this.#host = host; + this.#key = key; + this.#documentTreeRepo = new UmbTemplateTreeRepository(this.#host); // TODO: change to document repo + this.#documentDetailRepo = new UmbTemplateDetailRepository(this.#host); // TODO: change to document repo + + new UmbContextConsumerController(this.#host, UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#modalService = instance; + }); + } + + async execute() { + const { data } = await this.#documentTreeRepo.requestItems([this.#key]); + + if (data) { + const item = data[0]; + + const modalHandler = this.#modalService?.confirm({ + headline: `Delete ${item.name}`, + content: 'Are you sure you want to delete this item?', + color: 'danger', + confirmLabel: 'Delete', + }); + + modalHandler?.onClose().then(({ confirmed }) => { + if (confirmed) { + this.#documentDetailRepo.delete(this.#key); + } + }); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts index 8c172251e4..ae810f12e0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts @@ -1,5 +1,5 @@ import { CreateDocumentEntityAction } from './document-create.entity-action'; -import { DeleteDocumentEntityAction } from './document-delete.entity-action'; +import { TrashDocumentEntityAction } from './document-trash.entity-action'; import { PublishDocumentEntityAction } from './document-publish.entity-action'; import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; @@ -17,13 +17,13 @@ const entityActions: Array = [ }, { type: 'entityAction', - alias: 'Umb.EntityAction.Document.Delete', - name: 'Delete Document Entity Action ', + alias: 'Umb.EntityAction.Document.Trash', + name: 'Trash Document Entity Action ', meta: { entityType: 'document', icon: 'umb:trash', - label: 'Delete', - api: DeleteDocumentEntityAction, + label: 'Trash', + api: TrashDocumentEntityAction, }, }, { From 32690751b5cfd79267949e5f756e6a04f4be6d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 2 Feb 2023 10:49:57 +0100 Subject: [PATCH 021/174] implement document tree repository --- .../consume/context-consumer.controller.ts | 1 + .../context-api/consume/context-consumer.ts | 26 ++++- .../src/backoffice/backoffice.element.ts | 2 +- .../documents/document.tree.store.ts | 103 ------------------ .../tree/data/document.tree.repository.ts | 99 +++++++++++++++++ .../tree/data/document.tree.store.ts | 93 ++++++++++++++++ .../document.tree.data.source.interface.ts | 8 ++ .../data/sources/document.tree.server.data.ts | 97 +++++++++++++++++ .../documents/documents/tree/manifests.ts | 6 +- .../input-document-picker.element.ts | 6 +- 10 files changed, 329 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.data.source.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.server.data.ts diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts index 534cd6a04f..fe96b6e817 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.controller.ts @@ -21,6 +21,7 @@ export class UmbContextConsumerController } public destroy() { + super.destroy(); if (this.host) { this.host.removeController(this); } diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts index ab627326ad..0fe4fc33d8 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.ts @@ -7,6 +7,11 @@ import { UmbContextRequestEventImplementation, UmbContextCallback } from './cont * @class UmbContextConsumer */ export class UmbContextConsumer { + + + _promise?: Promise; + _promiseResolver?: (instance:T) => void; + private _instance?: T; get instance() { return this._instance; @@ -27,16 +32,23 @@ export class UmbContextConsumer, - private _callback: UmbContextCallback + private _callback?: UmbContextCallback ) { this._contextAlias = _contextAlias.toString(); } - private _onResponse = (instance: T) => { + protected _onResponse = (instance: T) => { this._instance = instance; - this._callback(instance); + this._callback?.(instance); + this._promiseResolver?.(instance); }; + public asPromise() { + return this._promise || (this._promise = new Promise((resolve) => { + this._instance ? resolve(this._instance) : (this._promiseResolver = resolve); + })); + } + /** * @memberof UmbContextConsumer */ @@ -63,4 +75,12 @@ export class UmbContextConsumer('UmbDocumentTreeStore'); - - -/** - * @export - * @class UmbDocumentStore - * @extends {UmbStoreBase} - * @description - Data Store for Documents - */ -export class UmbDocumentTreeStore extends UmbStoreBase implements UmbTreeStore { - - - private _data = new ArrayState([], (x) => x.key); - - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString()); - } - - // TODO: how do we handle trashed items? - async trash(keys: Array) { - // TODO: use backend cli when available. - const res = await fetch('/umbraco/management/api/v1/document/trash', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - const data = await res.json(); - this._data.append(data); - } - - async move(keys: Array, destination: string) { - // TODO: use backend cli when available. - const res = await fetch('/umbraco/management/api/v1/document/move', { - method: 'POST', - body: JSON.stringify({ keys, destination }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const data = await res.json(); - this._data.append(data); - } - - getTreeRoot() { - tryExecuteAndNotify(this._host, DocumentResource.getTreeDocumentRoot({})).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: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - return this._data.getObservablePart((items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); - } - - getTreeItemChildren(key: string) { - tryExecuteAndNotify( - this._host, - DocumentResource.getTreeDocumentChildren({ - 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); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - return this._data.getObservablePart((items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); - } - - getTreeItems(keys: Array) { - if (keys?.length > 0) { - tryExecuteAndNotify( - this._host, - DocumentResource.getTreeDocumentItem({ - 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/documents/documents/tree/data/document.tree.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts new file mode 100644 index 0000000000..55ce9f8f0e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts @@ -0,0 +1,99 @@ +import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; +import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; +import type { DocumentTreeDataSource } from './sources/document.tree.data.source.interface'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import type { UmbTreeRepository } from '@umbraco-cms/models'; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbDocumentTreeRepository implements UmbTreeRepository { + + #init!: Promise; + + #host: UmbControllerHostInterface; + #source: DocumentTreeDataSource; + + #store?: UmbDocumentTreeStore; + + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + // TODO: figure out how spin up get the correct data source + this.#source = new DocumentTreeServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; + }).asPromise() + ]) + } + + + + // TODO: Trash + // TODO: Move + + + async requestRootItems() { + await this.#init; + + const { data, error } = await this.#source.getRootItems(); + + if (data) { + this.#store?.appendItems(data.items); + } + + return { data, error }; + } + + async requestChildrenOf(parentKey: string | null) { + await this.#init; + + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#source.getChildrenOf(parentKey); + + if (data) { + this.#store?.appendItems(data.items); + } + + return { data, error }; + } + + async requestItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#source.getItems(keys); + + return { data, error }; + } + + async rootItems() { + await this.#init; + return this.#store!.rootItems; + } + + async childrenOf(parentKey: string | null) { + await this.#init; + return this.#store!.childrenOf(parentKey); + } + + async items(keys: Array) { + await this.#init; + return this.#store!.items(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.store.ts new file mode 100644 index 0000000000..730052a739 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.store.ts @@ -0,0 +1,93 @@ +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 UmbDocumentTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Templates + */ +// TODO: consider if tree store could be turned into a general EntityTreeStore class? +export class UmbDocumentTreeStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbDocumentTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbDocumentTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbDocumentTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} key + * @param {Partial} data + * @memberof UmbDocumentTreeStore + */ + 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 UmbDocumentTreeStore + */ + removeItem(key: string) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.remove([key]); + } + } + + /** + * An observable to observe the root items + * @memberof UmbDocumentTreeStore + */ + rootItems = 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 UmbDocumentTreeStore + */ + 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 UmbDocumentTreeStore + */ + items(keys: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} + +export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbDocumentTreeStore.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.data.source.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.data.source.interface.ts new file mode 100644 index 0000000000..3ccc10cf8c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.data.source.interface.ts @@ -0,0 +1,8 @@ +import { EntityTreeItem, PagedEntityTreeItem } from "@umbraco-cms/backend-api"; +import type { DataSourceResponse } from "@umbraco-cms/models"; + +export interface DocumentTreeDataSource { + getRootItems(): Promise>; + getChildrenOf(parentKey: string): Promise>; + getItems(key: Array): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.server.data.ts new file mode 100644 index 0000000000..77e162c56d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/sources/document.tree.server.data.ts @@ -0,0 +1,97 @@ +import type { DocumentTreeDataSource } from './document.tree.data.source.interface'; +import { ProblemDetails, DocumentResource } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Document tree that fetches data from the server + * @export + * @class DocumentTreeServerDataSource + * @implements {DocumentTreeDataSource} + */ +export class DocumentTreeServerDataSource implements DocumentTreeDataSource { + #host: UmbControllerHostInterface; + + + // TODO: how do we handle trashed items? + async trashItems(keys: Array) { + // TODO: use backend cli when available. + return tryExecuteAndNotify(this.#host, fetch('/umbraco/management/api/v1/document/trash', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + })); + } + + async moveItems(keys: Array, destination: string) { + // TODO: use backend cli when available. + return tryExecuteAndNotify(this.#host, fetch('/umbraco/management/api/v1/document/move', { + method: 'POST', + body: JSON.stringify({ keys, destination }), + headers: { + 'Content-Type': 'application/json', + }, + })); + } + + + /** + * Creates an instance of DocumentTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof DocumentTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof DocumentTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, DocumentResource.getTreeDocumentRoot({})); + } + + /** + * Fetches the children of a given parent key from the server + * @param {(string | null)} parentKey + * @return {*} + * @memberof DocumentTreeServerDataSource + */ + async getChildrenOf(parentKey: string | null) { + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + DocumentResource.getTreeDocumentChildren({ + parentKey, + }) + ); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof DocumentTreeServerDataSource + */ + async getItems(keys: Array) { + if (keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + DocumentResource.getTreeDocumentItem({ + key: keys, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts index 676ffe6c07..ff537562a9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts @@ -1,4 +1,5 @@ -import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../document.tree.store'; +import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './data/document.tree.store'; +import { UmbDocumentTreeRepository } from './data/document.tree.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.Documents'; @@ -9,7 +10,8 @@ const tree: ManifestTree = { name: 'Documents Tree', meta: { storeAlias: UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString(), - }, + repository: UmbDocumentTreeRepository, + } }; const treeItemActions: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts index d02dc7ed63..de1c6ee91b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts @@ -4,8 +4,8 @@ import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; -import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../../../backoffice/documents/documents/document.tree.store'; -import type { UmbDocumentTreeStore } from '../../../../backoffice/documents/documents/document.tree.store'; +import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../../documents/documents/tree/data/document.tree.store'; +import type { UmbDocumentTreeStore } from '../../../documents/documents/tree/data/document.tree.store'; import { UmbLitElement } from '@umbraco-cms/element'; import type { DocumentTreeItem, FolderTreeItem } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; @@ -115,7 +115,7 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen if (!this._documentStore) return; // TODO: consider changing this to the list data endpoint when it is available - this._pickedItemsObserver = this.observe(this._documentStore.getTreeItems(this._selectedKeys), (items) => { + this._pickedItemsObserver = this.observe(this._documentStore.items(this._selectedKeys), (items) => { this._items = items; }); } From d5c44410181087369a153e125b3946121cd11d42 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 2 Feb 2023 12:46:45 +0100 Subject: [PATCH 022/174] render actions menu button --- .../body-layout/body-layout.element.ts | 4 +- .../workspace-layout.element.ts | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts index f165f804c0..7df5bb2592 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts @@ -117,8 +117,8 @@ export class UmbBodyLayout extends LitElement { this._tabsSlotHasChildren = this.#hasNodes(e); }}> { this._actionsMenuSlotHasChildren = this.#hasNodes(e); }}> diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts index 203edddfd7..c585c09ca6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts @@ -61,6 +61,22 @@ export class UmbWorkspaceLayout extends UmbLitElement { display: flex; gap: var(--uui-size-space-2); } + + #action-menu-popover { + display: block; + } + #action-menu-dropdown { + overflow: hidden; + z-index: -1; + background-color: var(--uui-combobox-popover-background-color, var(--uui-color-surface)); + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + width: 100%; + height: 100%; + box-sizing: border-box; + box-shadow: var(--uui-shadow-depth-3); + width: 500px; + } `, ]; @@ -219,9 +235,29 @@ export class UmbWorkspaceLayout extends UmbLitElement { `; } + @state() + private _actionMenuIsOpen = false; + + #close() { + this._actionMenuIsOpen = false; + } + + #open() { + this._actionMenuIsOpen = true; + } + #renderActionsMenu() { - return html`
- ${this._entityActions?.map((manifest) => html``)} + return html` +
+ + +
+ + ${this._entityActions?.map((manifest) => html``)} + +
+
+
`; } } From 56f79d1daeb03d8005344d1ce9daf3631dec1db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 2 Feb 2023 13:04:47 +0100 Subject: [PATCH 023/174] initial merge --- .../libs/extensions-registry/tree.models.ts | 4 +- .../libs/models/index.ts | 21 +- .../detail-repository.interface.ts | 27 +++ .../repositories/tree-repository.interface.ts | 20 ++ .../document.repository.ts} | 11 +- .../documents/documents/tree/manifests.ts | 4 +- .../repository/template.repository.ts | 225 ++++++++++++++++++ .../tree/data/template.tree.repository.ts | 1 - .../data/template.detail.repository.ts | 2 + 9 files changed, 285 insertions(+), 30 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/libs/repositories/detail-repository.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/repositories/tree-repository.interface.ts rename src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/{tree/data/document.tree.repository.ts => repository/document.repository.ts} (83%) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts index 38bc2a54bb..017bede8bd 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/tree.models.ts @@ -1,5 +1,5 @@ import type { ManifestBase } from './models'; -import type { UmbTreeRepositoryFactory } from '@umbraco-cms/models'; +import type { UmbRepositoryFactory } from '@umbraco-cms/models'; export interface ManifestTree extends ManifestBase { type: 'tree'; @@ -8,5 +8,5 @@ export interface ManifestTree extends ManifestBase { export interface MetaTree { storeAlias?: string; - repository?: UmbTreeRepositoryFactory; + repository?: UmbRepositoryFactory; } diff --git a/src/Umbraco.Web.UI.Client/libs/models/index.ts b/src/Umbraco.Web.UI.Client/libs/models/index.ts index 9e91bcb3df..dd5781d98f 100644 --- a/src/Umbraco.Web.UI.Client/libs/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/models/index.ts @@ -7,6 +7,7 @@ import { PagedEntityTreeItem, ProblemDetails, } from '@umbraco-cms/backend-api'; +import { UmbTreeRepository } from 'libs/repositories/tree-repository.interface'; import { Observable } from 'rxjs'; // Extension Manifests @@ -159,24 +160,6 @@ export interface DataSourceResponse { } // TODO; figure out why we can't add UmbControllerHostInterface as host type -export interface UmbTreeRepositoryFactory { +export interface UmbRepositoryFactory { new (host: any): UmbTreeRepository; } - -export interface UmbTreeRepository { - requestRootItems: () => Promise<{ - data: PagedEntityTreeItem | undefined; - error: ProblemDetails | undefined; - }>; - requestChildrenOf: (parentKey: string | null) => Promise<{ - data: PagedEntityTreeItem | undefined; - error: ProblemDetails | undefined; - }>; - requestItems: (keys: string[]) => Promise<{ - data: Array | undefined; - error: ProblemDetails | undefined; - }>; - rootItems: () => Promise>; - childrenOf: (parentKey: string | null) => Promise>; - items: (keys: string[]) => Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/libs/repositories/detail-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repositories/detail-repository.interface.ts new file mode 100644 index 0000000000..3c1b9a828f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repositories/detail-repository.interface.ts @@ -0,0 +1,27 @@ +import type { ProblemDetails } from "@umbraco-cms/backend-api"; + +export interface UmbDetailRepository { + + createDetailsScaffold(parentKey: string | null): Promise<{ + data?: DetailType; + error?: ProblemDetails; + }> + + requestDetails(key: string): Promise<{ + data?: DetailType; + error?: ProblemDetails; + }> + + create(data: DetailType): Promise<{ + error?: ProblemDetails; + }> + + save(data: DetailType): Promise<{ + error?: ProblemDetails; + }> + + delete(key: string): Promise<{ + error?: ProblemDetails; + }> + +} diff --git a/src/Umbraco.Web.UI.Client/libs/repositories/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repositories/tree-repository.interface.ts new file mode 100644 index 0000000000..c084828171 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repositories/tree-repository.interface.ts @@ -0,0 +1,20 @@ +import type { Observable } from "rxjs"; +import { EntityTreeItem, PagedEntityTreeItem, ProblemDetails } from "@umbraco-cms/backend-api"; + +export interface UmbTreeRepository { + requestRootItems: () => Promise<{ + data: PagedEntityTreeItem | undefined; + error: ProblemDetails | undefined; + }>; + requestChildrenOf: (parentKey: string | null) => Promise<{ + data: PagedEntityTreeItem | undefined; + error: ProblemDetails | undefined; + }>; + requestItems: (keys: string[]) => Promise<{ + data: Array | undefined; + error: ProblemDetails | undefined; + }>; + rootItems: () => Promise>; + childrenOf: (parentKey: string | null) => Promise>; + items: (keys: string[]) => Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts similarity index 83% rename from src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index 55ce9f8f0e..dcddd65a08 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/data/document.tree.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -1,17 +1,16 @@ -import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; -import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; -import type { DocumentTreeDataSource } from './sources/document.tree.data.source.interface'; +import { DocumentTreeServerDataSource } from '../tree/data/sources/document.tree.server.data'; +import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../tree/data/document.tree.store'; +import type { DocumentTreeDataSource } from '../tree/data/sources/document.tree.data.source.interface'; 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'; +import type { UmbTreeRepository } from 'libs/repositories/tree-repository.interface'; // Move to documentation / JSdoc /* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ // element -> context -> repository -> (store) -> data source // All methods should be async and return a promise. Some methods might return an observable as part of the promise response. -export class UmbDocumentTreeRepository implements UmbTreeRepository { +export class UmbDocumentRepository implements UmbTreeRepository { #init!: Promise; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts index ff537562a9..12fa66a9e4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './data/document.tree.store'; -import { UmbDocumentTreeRepository } from './data/document.tree.repository'; +import { UmbDocumentRepository } from '../repository/document.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.Documents'; @@ -10,7 +10,7 @@ const tree: ManifestTree = { name: 'Documents Tree', meta: { storeAlias: UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString(), - repository: UmbDocumentTreeRepository, + repository: UmbDocumentRepository, } }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts new file mode 100644 index 0000000000..9199a0c364 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts @@ -0,0 +1,225 @@ +import { UmbTemplateDetailServerDataSource } from '../workspace/data/sources/template.detail.server.data'; +import { TemplateTreeServerDataSource } from '../tree/data/sources/template.tree.server.data'; +import { UmbTemplateDetailStore, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN } from '../workspace/data/template.detail.store'; +import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from './template.tree.store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails, Template } from '@umbraco-cms/backend-api'; +import { UmbDetailRepository } from 'libs/repositories/detail-repository.interface'; +import { UmbTreeRepository } from 'libs/repositories/tree-repository.interface'; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailRepository