diff --git a/src/Umbraco.Web.UI.Client/.storybook/preview.js b/src/Umbraco.Web.UI.Client/.storybook/preview.js index 49838d94cd..10c6daa7b0 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/preview.js +++ b/src/Umbraco.Web.UI.Client/.storybook/preview.js @@ -11,9 +11,10 @@ import { html } from 'lit-html'; import { initialize, mswDecorator } from 'msw-storybook-addon'; import { setCustomElements } from '@storybook/web-components'; +import { UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN, UmbDataTypeDetailStore } from '../src/backoffice/settings/data-types/data-type.detail.store'; +import { UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN, UmbDocumentTypeDetailStore } from '../src/backoffice/documents/document-types/document-type.detail.store'; + import customElementManifests from '../custom-elements.json'; -import { STORE_ALIAS as dataTypeAlias, UmbDataTypeStore } from '../src/backoffice/settings/data-types/data-type.store'; -import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types/document-type.store'; import { UmbIconStore } from '../libs/store/icon/icon.store'; import { onUnhandledRequest } from '../src/core/mocks/browser'; import { handlers } from '../src/core/mocks/browser-handlers'; @@ -54,11 +55,11 @@ customElements.define('umb-storybook', UmbStoryBookElement); const storybookProvider = (story) => html` ${story()} `; const dataTypeStoreProvider = (story) => html` - ${story()} + ${story()} `; const documentTypeStoreProvider = (story) => html` - ${story()} `; diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.method.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.method.ts new file mode 100644 index 0000000000..638008ffb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.method.ts @@ -0,0 +1,32 @@ +/** + * @export + * @method appendToFrozenArray + * @param {Observable} source - RxJS Subject to use for this Observable. + * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. + * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. + * @description - Creates a RxJS Observable from RxJS Subject. + * @example Example append new entry for a UniqueBehaviorSubject which is an array. Where the key is unique and the item will be updated if matched with existing. + * const entry = {key: 'myKey', value: 'myValue'}; + * const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.key === key); + * mySubject.next(newDataSet); + */ + + + + + +export function appendToFrozenArray(data: T[], entry: T, getUniqueMethod?: (entry: T) => unknown): T[] { + const unFrozenDataSet = [...data]; + if (getUniqueMethod) { + const unique = getUniqueMethod(entry); + const indexToReplace = unFrozenDataSet.findIndex((x) => getUniqueMethod(x) === unique); + if (indexToReplace !== -1) { + unFrozenDataSet[indexToReplace] = entry; + } else { + unFrozenDataSet.push(entry); + } + } else { + unFrozenDataSet.push(entry); + } + return unFrozenDataSet; +} diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts index 8b9c23fdbd..a742f8b666 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts @@ -4,3 +4,4 @@ export * from './unique-behavior-subject'; export * from './unique-array-behavior-subject'; export * from './unique-object-behavior-subject'; export * from './create-observable-part.method' +export * from './append-to-frozen-array.method' diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.test.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.test.ts index ab98b2ac50..8ab7688698 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.test.ts @@ -1,6 +1,6 @@ import { expect } from '@open-wc/testing'; -import { createObservablePart } from '@umbraco-cms/observable-api'; import { UniqueArrayBehaviorSubject } from './unique-array-behavior-subject'; +import { createObservablePart } from '@umbraco-cms/observable-api'; describe('UniqueArrayBehaviorSubject', () => { @@ -16,7 +16,7 @@ describe('UniqueArrayBehaviorSubject', () => { {key: '2', another: 'myValue2'}, {key: '3', another: 'myValue3'} ]; - subject = new UniqueArrayBehaviorSubject(initialData, (a, b) => a.key === b.key); + subject = new UniqueArrayBehaviorSubject(initialData, x => x.key); }); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.ts index 2ab466b647..94267f6379 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-array-behavior-subject.ts @@ -1,4 +1,5 @@ -import { appendToFrozenArray, UniqueBehaviorSubject } from "./unique-behavior-subject"; +import { UniqueBehaviorSubject } from "./unique-behavior-subject"; +import { appendToFrozenArray } from "./append-to-frozen-array.method"; /** * @export @@ -13,14 +14,41 @@ import { appendToFrozenArray, UniqueBehaviorSubject } from "./unique-behavior-su export class UniqueArrayBehaviorSubject extends UniqueBehaviorSubject { - constructor(initialData: T[], private _uniqueCompare?: (existingEntry: T, newEntry: T) => boolean) { + constructor(initialData: T[], private _getUnique?: (entry: T) => unknown) { super(initialData); } + /** + * @method append + * @param {unknown} unique - The unique value to remove. + * @description - Remove some new data of this Subject. + * @example Example remove entry with key '1' + * const data = [ + * { key: 1, value: 'foo'}, + * { key: 2, value: 'bar'} + * ]; + * const mySubject = new UniqueArrayBehaviorSubject(data, (x) => x.key); + * mySubject.remove([1]); + */ + remove(uniques: unknown[]) { + const unFrozenDataSet = [...this.getValue()]; + if (this._getUnique) { + uniques.forEach( unique => + unFrozenDataSet.filter(x => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return this._getUnique(x) !== unique; + }) + ); + + this.next(unFrozenDataSet); + } + } + /** * @method append * @param {Partial} partialData - A object containing some of the data for this Subject. - * @description - Append some new data to this Object. + * @description - Append some new data to this Subject. * @example Example append some data. * const data = [ * { key: 1, value: 'foo'}, @@ -30,13 +58,13 @@ export class UniqueArrayBehaviorSubject extends UniqueBehaviorSubject { * mySubject.append({ key: 1, value: 'replaced-foo'}); */ appendOne(entry: T) { - this.next(appendToFrozenArray(this.getValue(), entry, this._uniqueCompare)) + this.next(appendToFrozenArray(this.getValue(), entry, this._getUnique)) } /** * @method append * @param {T[]} entries - A array of new data to be added in this Subject. - * @description - Append some new data to this Object, if it compares to existing data it will replace it. + * @description - Append some new data to this Subject, if it compares to existing data it will replace it. * @example Example append some data. * const data = [ * { key: 1, value: 'foo'}, @@ -49,6 +77,7 @@ export class UniqueArrayBehaviorSubject extends UniqueBehaviorSubject { * ]); */ append(entries: T[]) { + // TODO: stop calling appendOne for each but make sure to handle this in one. entries.forEach(x => this.appendOne(x)) } } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-behavior-subject.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-behavior-subject.ts index 032ac040e8..20af12b3b1 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/unique-behavior-subject.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/unique-behavior-subject.ts @@ -28,36 +28,6 @@ export function naiveObjectComparison(objOne: any, objTwo: any): boolean { -/** - * @export - * @method appendToFrozenArray - * @param {Observable} source - RxJS Subject to use for this Observable. - * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. - * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. - * @description - Creates a RxJS Observable from RxJS Subject. - * @example Example append new entry for a UniqueBehaviorSubject which is an array. Where the key is unique and the item will be updated if matched with existing. - * const entry = {key: 'myKey', value: 'myValue'}; - * const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.key === key); - * mySubject.next(newDataSet); - */ -export function appendToFrozenArray(data: T[], entry: T, uniqueMethod?: (existingEntry: T, newEntry: T) => boolean): T[] { - const unFrozenDataSet = [...data]; - if(uniqueMethod) { - const indexToReplace = unFrozenDataSet.findIndex((x) => uniqueMethod(x, entry)); - if(indexToReplace !== -1) { - unFrozenDataSet[indexToReplace] = entry; - } else { - unFrozenDataSet.push(entry); - } - } else { - unFrozenDataSet.push(entry); - } - return unFrozenDataSet; -} - - - - export type MappingFunction = (mappable: T) => R; export type MemoizationFunction = (previousResult: R, currentResult: R) => boolean; diff --git a/src/Umbraco.Web.UI.Client/libs/store/index.ts b/src/Umbraco.Web.UI.Client/libs/store/index.ts index 8f76b6f678..2c8f2e1e80 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/index.ts @@ -1,2 +1,3 @@ export * from './icon/icon.store'; export * from './store'; +export * from './store-base'; diff --git a/src/Umbraco.Web.UI.Client/libs/store/store-base.ts b/src/Umbraco.Web.UI.Client/libs/store/store-base.ts new file mode 100644 index 0000000000..97430508ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/store/store-base.ts @@ -0,0 +1,11 @@ +import { UmbContextProviderController } from "../context-api/provide/context-provider.controller"; +import { UmbControllerHostInterface } from "../controller/controller-host.mixin"; + +export class UmbStoreBase { + + + constructor (protected _host: UmbControllerHostInterface, public readonly storeAlias: string) { + new UmbContextProviderController(_host, storeAlias, this); + } + +} diff --git a/src/Umbraco.Web.UI.Client/libs/store/store.ts b/src/Umbraco.Web.UI.Client/libs/store/store.ts index f6fc600e5a..6a2b3c0122 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/store.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/store.ts @@ -1,94 +1,33 @@ import type { Observable } from 'rxjs'; -import { UniqueBehaviorSubject } from '@umbraco-cms/observable-api'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; export interface UmbDataStoreIdentifiers { key?: string; [more: string]: any; } -export interface UmbDataStore { +export interface UmbDataStore { readonly storeAlias: string; - readonly items: Observable>; - updateItems(items: Array): void; } -export interface UmbTreeDataStore extends UmbDataStore { +export interface UmbTreeStore extends UmbDataStore { getTreeRoot(): Observable>; getTreeItemChildren(key: string): Observable>; } -/** - * @export - * @class UmbDataStoreBase - * @implements {UmbDataStore} - * @template T - * @description - Base class for Data Stores - */ -export abstract class UmbDataStoreBase implements UmbDataStore { - public abstract readonly storeAlias: string; - - protected _items = new UniqueBehaviorSubject(>[]); - public readonly items = this._items.asObservable(); - - protected host: UmbControllerHostInterface; - - constructor(host: UmbControllerHostInterface) { - this.host = host; - } - - /** - * @description - Delete items from the store. - * @param {Array} keys - * @memberof UmbDataStoreBase - */ - public deleteItems(keys: Array): void { - const remainingItems = this._items.getValue().filter((item) => item.key && keys.includes(item.key) === false); - this._items.next(remainingItems); - } - - /** - * @description - Update the store with new items. Existing items are updated, new items are added, old are kept. Items are matched by the compareKey. - * @param {Array} items - * @param {keyof T} [compareKey='key'] - * @memberof UmbDataStoreBase - */ - public updateItems(items: Array, compareKey: keyof T = 'key'): void { - const newData = [...this._items.getValue()]; - items.forEach((newItem) => { - const storedItemIndex = newData.findIndex((item) => item[compareKey] === newItem[compareKey]); - if (storedItemIndex !== -1) { - newData[storedItemIndex] = newItem; - } else { - newData.push(newItem); - } - }); - - this._items.next(newData); - } -} - -/** - * @export - * @class UmbNodeStoreBase - * @implements {UmbDataStore} - * @template T - * @description - Base class for Data Stores - */ -export abstract class UmbNodeStoreBase extends UmbDataStoreBase { +export interface UmbContentStore extends UmbDataStore { /** * @description - Request data by key. The data is added to the store and is returned as an Observable. * @param {string} key - * @return {*} {(Observable)} + * @return {*} {(Observable)} * @memberof UmbDataStoreBase */ - abstract getByKey(key: string): Observable; + getByKey(key: string): Observable; /** * @description - Save data. * @param {object} data * @return {*} {(Promise)} - * @memberof UmbNodeStoreBase + * @memberof UmbContentStore */ - abstract save(data: T[]): Promise; + save(data: T[]): Promise; } diff --git a/src/Umbraco.Web.UI.Client/public/mockServiceWorker.js b/src/Umbraco.Web.UI.Client/public/mockServiceWorker.js index 70f0a2b994..671ec2cbd0 100644 --- a/src/Umbraco.Web.UI.Client/public/mockServiceWorker.js +++ b/src/Umbraco.Web.UI.Client/public/mockServiceWorker.js @@ -2,7 +2,7 @@ /* tslint:disable */ /** - * Mock Service Worker (0.49.2). + * Mock Service Worker (0.49.3). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. 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 606c2efbec..011b537e8e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -2,34 +2,38 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbLitElement } from '@umbraco-cms/element'; + import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../core/modal'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from './users/users/user.store'; -import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from './users/user-groups/user-group.store'; +import { UmbUserStore } from './users/users/user.store'; +import { UmbUserGroupStore } from './users/user-groups/user-group.store'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './users/current-user/current-user.store'; import { UmbCurrentUserHistoryStore, UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, } from './users/current-user/current-user-history.store'; -import { - UmbDocumentTypeStore, - UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN, -} from './documents/document-types/document-type.store'; -import { UmbMediaTypeStore, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN } from './media/media-types/media-type.store'; -import { UmbMemberTypeStore, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN } from './members/member-types/member-type.store'; -import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './documents/documents/document.store'; -import { UmbMediaStore, UMB_MEDIA_STORE_CONTEXT_TOKEN } from './media/media/media.store'; -import { UmbMemberGroupStore, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN } from './members/member-groups/member-group.store'; -import { UmbDictionaryStore, UMB_DICTIONARY_STORE_CONTEXT_TOKEN } from './translation/dictionary/dictionary.store'; -import { - UmbDocumentBlueprintStore, - UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN, -} from './documents/document-blueprints/document-blueprint.store'; +import {UmbDocumentTypeDetailStore} from './documents/document-types/document-type.detail.store'; +import {UmbDocumentTypeTreeStore} from './documents/document-types/document-type.tree.store'; +import { UmbMediaTypeDetailStore } from './media/media-types/media-type.detail.store'; +import { UmbMediaTypeTreeStore } from './media/media-types/media-type.tree.store'; +import { UmbDocumentDetailStore } from './documents/documents/document.detail.store'; +import { UmbDocumentTreeStore } from './documents/documents/document.tree.store'; +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 { 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'; +import { UmbDocumentBlueprintTreeStore } from './documents/document-blueprints/document-blueprint.tree.store'; import { UmbSectionStore, UMB_SECTION_STORE_CONTEXT_TOKEN } from './shared/components/section/section.store'; -import { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from './settings/data-types/data-type.store'; -import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; -import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbDataTypeDetailStore } from './settings/data-types/data-type.detail.store'; +import { UmbDataTypeTreeStore } from './settings/data-types/data-type.tree.store'; + // Domains import './settings'; @@ -67,19 +71,29 @@ export class UmbBackofficeElement extends UmbLitElement { // TODO: find a way this is possible outside this element. It needs to be possible to register stores in extensions this.provideContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, new UmbCurrentUserStore()); - this.provideContext(UMB_DOCUMENT_STORE_CONTEXT_TOKEN, new UmbDocumentStore(this)); - this.provideContext(UMB_MEDIA_STORE_CONTEXT_TOKEN, new UmbMediaStore(this)); - this.provideContext(UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, new UmbDataTypeStore(this)); - this.provideContext(UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN, new UmbDocumentTypeStore(this)); - this.provideContext(UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN, new UmbMediaTypeStore(this)); - this.provideContext(UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN, new UmbMemberTypeStore(this)); - this.provideContext(UMB_USER_STORE_CONTEXT_TOKEN, new UmbUserStore(this)); - this.provideContext(UMB_USER_GROUP_STORE_CONTEXT_TOKEN, new UmbUserGroupStore(this)); - this.provideContext(UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN, new UmbMemberGroupStore(this)); + + new UmbDocumentDetailStore(this); + new UmbDocumentTreeStore(this); + new UmbMediaDetailStore(this); + new UmbMediaTreeStore(this); + new UmbDataTypeDetailStore(this); + new UmbDataTypeTreeStore(this); + new UmbUserStore(this); + new UmbMediaTypeDetailStore(this); + new UmbMediaTypeTreeStore(this); + new UmbDocumentTypeDetailStore(this); + new UmbDocumentTypeTreeStore(this); + new UmbMemberTypeDetailStore(this); + new UmbMemberTypeTreeStore(this); + new UmbUserGroupStore(this); + new UmbMemberGroupStore(this); + new UmbDictionaryDetailStore(this); + new UmbDictionaryTreeStore(this); + new UmbDocumentBlueprintDetailStore(this); + new UmbDocumentBlueprintTreeStore(this); + this.provideContext(UMB_SECTION_STORE_CONTEXT_TOKEN, new UmbSectionStore()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); - this.provideContext(UMB_DICTIONARY_STORE_CONTEXT_TOKEN, new UmbDictionaryStore(this)); - this.provideContext(UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN, new UmbDocumentBlueprintStore(this)); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts new file mode 100644 index 0000000000..8c42284bfb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts @@ -0,0 +1,99 @@ +import type { DocumentBlueprintDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DocumentBlueprint_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentBlueprintDetailStore'); + + +/** + * @export + * @class UmbDocumentBlueprintDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Document Blueprints + */ +export class UmbDocumentBlueprintDetailStore extends UmbStoreBase { + + + // TODO: use the right type: + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DocumentBlueprint_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbDocumentBlueprintDetailStore + */ + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/document/document-blueprint/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + }); + + return createObservablePart(this.#data, (documents) => + documents.find((document) => document.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a DocumentBlueprint. + * @param {Array} Dictionaries + * @memberof UmbDocumentBlueprintDetailStore + * @return {*} {Promise} + */ + save(data: DocumentBlueprintDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/document-blueprint/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this.#data.append(data); + }); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDocumentBlueprintDetailStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/document-blueprint/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.store.ts deleted file mode 100644 index 32d9437b00..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.store.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbNodeStoreBase } from '@umbraco-cms/store'; -import type { DocumentBlueprintDetails, DocumentDetails } from '@umbraco-cms/models'; -import { DocumentBlueprintTreeItem } from '@umbraco-cms/backend-api'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export type UmbDocumentStoreItemType = DocumentBlueprintDetails | DocumentBlueprintTreeItem; - -export const STORE_ALIAS = 'UmbDocumentBlueprintStore'; - -const isDocumentBlueprintDetails = ( - documentBlueprint: DocumentBlueprintDetails | DocumentBlueprintTreeItem -): documentBlueprint is DocumentBlueprintDetails => { - return (documentBlueprint as DocumentBlueprintDetails).data !== undefined; -}; - -/** - * @export - * @class UmbDocumentStore - * @extends {UmbDocumentStoreBase} - * @description - Data Store for Documents - */ -export class UmbDocumentBlueprintStore extends UmbNodeStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - // TODO: implement call to end point - return this.items.pipe( - map( - (documentBlueprints) => - (documentBlueprints.find( - (documentBlueprint) => documentBlueprint.key === key && isDocumentBlueprintDetails(documentBlueprint) - ) as DocumentDetails) || null - ) - ); - } - - // TODO: implement call to end point - save(): any { - return; - } -} - -export const UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts new file mode 100644 index 0000000000..1f6018f2dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts @@ -0,0 +1,98 @@ +import { DocumentBlueprintResource, DocumentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DocumentBlueprint_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentBlueprintTreeStore'); + + +/** + * @export + * @class UmbDocumentBlueprintTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Document Blueprints + */ +export class UmbDocumentBlueprintTreeStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DocumentBlueprint_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Document Blueprint Type. + * @param {string[]} keys + * @memberof UmbDocumentBlueprintsStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/data-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, DocumentBlueprintResource.getTreeDocumentBlueprintRoot({})).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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); + } + + getTreeItemChildren(key: string) { + /* + tryExecuteAndNotify( + this._host, + DocumentBlueprintResource.getTreeDocumentBlueprintChildren({ + 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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); + } + + getTreeItems(keys: Array) { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + DocumentBlueprintResource.getTreeDocumentBlueprintItem({ + 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/documents/document-types/document-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.detail.store.ts new file mode 100644 index 0000000000..449005c578 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.detail.store.ts @@ -0,0 +1,98 @@ +import type { DocumentTypeDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentTypeDetailStore'); + + +/** + * @export + * @class UmbDocumentTypeDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Document Types + */ +export class UmbDocumentTypeDetailStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbDocumentTypesStore + */ + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/document/document-type/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + }); + + return createObservablePart(this.#data, (documentTypes) => + documentTypes.find((documentType) => documentType.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a Data Type. + * @param {Array} documentTypes + * @memberof UmbDocumentTypesStore + * @return {*} {Promise} + */ + save(data: DocumentTypeDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/document-type/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this.#data.append(data); + }); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDocumentTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/document-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.store.ts deleted file mode 100644 index f682b6ddb5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.store.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import { DocumentTypeResource, DocumentTypeTreeItem } from '@umbraco-cms/backend-api'; -import type { DocumentTypeDetails } from '@umbraco-cms/models'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export const isDocumentTypeDetails = ( - documentType: DocumentTypeDetails | DocumentTypeTreeItem -): documentType is DocumentTypeDetails => { - return (documentType as DocumentTypeDetails).properties !== undefined; -}; - -export type UmbDocumentTypeStoreItemType = DocumentTypeDetails | DocumentTypeTreeItem; - -export const STORE_ALIAS = 'UmbDocumentTypeStore'; - -/** - * @export - * @class UmbDocumentTypeStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Document Types - */ -export class UmbDocumentTypeStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - // TODO: use Fetcher API. - // TODO: only fetch if the data type is not in the store? - fetch(`/umbraco/backoffice/document-type/${key}`) - .then((res) => res.json()) - .then((data) => { - this.updateItems(data); - }); - - return this.items.pipe( - map( - (documentTypes) => - (documentTypes.find( - (documentType) => documentType.key === key && isDocumentTypeDetails(documentType) - ) as DocumentTypeDetails) || null - ) - ); - } - - async save(documentTypes: Array) { - // TODO: use Fetcher API. - try { - const res = await fetch('/umbraco/backoffice/document-type/save', { - method: 'POST', - body: JSON.stringify(documentTypes), - headers: { - 'Content-Type': 'application/json', - }, - }); - const json = await res.json(); - this.updateItems(json); - } catch (error) { - console.error('Save Document Type error', error); - } - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, DocumentTypeResource.getTreeDocumentTypeRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } - - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - DocumentTypeResource.getTreeDocumentTypeChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key))); - } -} - -export const UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.tree.store.ts new file mode 100644 index 0000000000..dbca0c235c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/document-type.tree.store.ts @@ -0,0 +1,94 @@ +import { DocumentTypeResource, DocumentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentTypeTreeStore'); + + +/** + * @export + * @class UmbDocumentTypeTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Data Types + */ +export class UmbDocumentTypeTreeStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDocumentTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/document-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, DocumentTypeResource.getTreeDocumentTypeRoot({})).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, + DocumentTypeResource.getTreeDocumentTypeChildren({ + 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: remove ignore when we know how to handle trashed items. + return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key)); + } + + getTreeItems(keys: Array) { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + DocumentTypeResource.getTreeDocumentTypeItem({ + 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/documents/document-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts index 694de8a0bf..f0b8d07917 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../document-type.store'; +import { UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from '../document-type.detail.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { @@ -6,7 +6,7 @@ const tree: ManifestTree = { alias: 'Umb.Tree.DocumentTypes', name: 'Document Types Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts index a60d48833d..0214a2a2c4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts @@ -1,10 +1,10 @@ import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; import { - UmbDocumentTypeStore, - UmbDocumentTypeStoreItemType, - UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/documents/document-types/document-type.store'; + UmbDocumentTypeDetailStore, + UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN, +} from '../document-type.detail.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { DocumentTypeDetails } from '@umbraco-cms/models'; const DefaultDocumentTypeData = { key: '', @@ -15,14 +15,14 @@ const DefaultDocumentTypeData = { parentKey: '', alias: '', properties: [], -} as UmbDocumentTypeStoreItemType; +} as DocumentTypeDetails; export class UmbWorkspaceDocumentTypeContext extends UmbWorkspaceContentContext< - UmbDocumentTypeStoreItemType, - UmbDocumentTypeStore + DocumentTypeDetails, + UmbDocumentTypeDetailStore > { constructor(host: UmbControllerHostInterface) { - super(host, DefaultDocumentTypeData, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString(), 'documentType'); + super(host, DefaultDocumentTypeData, UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'documentType'); } public setPropertyValue(alias: string, value: unknown) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts index 146230903f..1c845de142 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts @@ -3,15 +3,15 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; import { distinctUntilChanged } from 'rxjs'; import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; -import type { UmbDocumentTypeStoreItemType } from '../../../document-type.store'; import { UmbLitElement } from '@umbraco-cms/element'; +import type { DocumentTypeDetails } from '@umbraco-cms/models'; @customElement('umb-workspace-view-document-type-design') export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { static styles = [UUITextStyles, css``]; @state() - _documentType?: UmbDocumentTypeStoreItemType | null; + _documentType?: DocumentTypeDetails | null; private _workspaceContext?: UmbWorkspaceDocumentTypeContext; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.detail.store.ts new file mode 100644 index 0000000000..2b2d750a77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.detail.store.ts @@ -0,0 +1,80 @@ +import type { DocumentDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase, UmbContentStore } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentDetailStore'); + + +/** + * @export + * @class UmbDocumentStore + * @extends {UmbStoreBase} + * @description - Data Store for Documents + */ +export class UmbDocumentDetailStore extends UmbStoreBase implements UmbContentStore { + + + private _data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/document/details/${key}`) + .then((res) => res.json()) + .then((data) => { + this._data.append(data); + }); + + return createObservablePart(this._data, (documents) => + documents.find((document) => document.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + save(data: DocumentDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/document/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this._data.append(data); + }); + } + + // 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); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.store.ts deleted file mode 100644 index 04bc2cb05e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.store.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbNodeStoreBase } from '@umbraco-cms/store'; -import type { DocumentDetails } from '@umbraco-cms/models'; -import { DocumentResource, DocumentTreeItem, FolderTreeItem } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { createObservablePart } from '@umbraco-cms/observable-api'; - -export const isDocumentDetails = (document: DocumentDetails | DocumentTreeItem): document is DocumentDetails => { - return (document as DocumentDetails).data !== undefined; -}; - -export type UmbDocumentStoreItemType = DocumentDetails | DocumentTreeItem; - -// TODO: research how we write names of global consts. -export const STORE_ALIAS = 'UmbDocumentStore'; - -/** - * @export - * @class UmbDocumentStore - * @extends {UmbDocumentStoreBase} - * @description - Data Store for Documents - */ -export class UmbDocumentStore extends UmbNodeStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - // TODO: use backend cli when available. - fetch(`/umbraco/management/api/v1/document/details/${key}`) - .then((res) => res.json()) - .then((data) => { - this.updateItems(data); - }); - - /* - return this.items.pipe( - map( - (documents) => - (documents.find((document) => document.key === key && isDocumentDetails(document)) as DocumentDetails) || null - ) - ); - */ - - return createObservablePart( - this.items, - (documents) => - documents.find((document) => document.key === key && isDocumentDetails(document)) as DocumentDetails - ); - } - - // TODO: make sure UI somehow can follow the status of this action. - save(data: DocumentDetails[]): Promise { - // fetch from server and update store - // TODO: use Fetcher API. - let body: string; - - try { - body = JSON.stringify(data); - } catch (error) { - console.error(error); - return Promise.reject(); - } - - // TODO: use backend cli when available. - return fetch('/umbraco/management/api/v1/document/save', { - method: 'POST', - body: body, - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((res) => res.json()) - .then((data: Array) => { - this.updateItems(data); - }); - } - - // 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.updateItems(data); - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, DocumentResource.getTreeDocumentRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null && !item.isTrashed))); - } - - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - DocumentResource.getTreeDocumentChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key && !item.isTrashed))); - } - - getTreeItems(keys: Array): Observable> { - if (keys?.length > 0) { - tryExecuteAndNotify( - this.host, - DocumentResource.getTreeDocumentItem({ - key: keys, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data); - } - }); - } - - return this.items.pipe(map((items) => items.filter((item) => keys.includes(item.key ?? '')))); - } -} - -export const UMB_DOCUMENT_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.tree.store.ts new file mode 100644 index 0000000000..b46b06cbb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/document.tree.store.ts @@ -0,0 +1,90 @@ +import { DocumentResource, DocumentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentTreeStore'); + + +/** + * @export + * @class UmbDocumentStore + * @extends {UmbStoreBase} + * @description - Data Store for Documents + */ +export class UmbDocumentTreeStore extends UmbStoreBase { + + + private _data = new UniqueArrayBehaviorSubject([], (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); + } + + 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 createObservablePart(this._data, (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 createObservablePart(this._data, (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 createObservablePart(this._data, (items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/actions/action-document-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/actions/action-document-delete.element.ts index 9c635f9d6e..34a5180f84 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/actions/action-document-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/actions/action-document-delete.element.ts @@ -2,7 +2,8 @@ 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 { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from '../../document.store'; +import type { UmbDocumentDetailStore } from '../../document.detail.store'; +import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from '../../document.detail.store'; import UmbTreeItemActionElement from '../../../../shared/components/tree/action/tree-item-action.element'; @customElement('umb-tree-action-document-delete') @@ -10,7 +11,7 @@ export default class UmbTreeActionDocumentDeleteElement extends UmbTreeItemActio static styles = [UUITextStyles, css``]; private _modalService?: UmbModalService; - private _documentStore?: UmbDocumentStore; + private _documentStore?: UmbDocumentDetailStore; connectedCallback(): void { super.connectedCallback(); @@ -19,7 +20,7 @@ export default class UmbTreeActionDocumentDeleteElement extends UmbTreeItemActio this._modalService = modalService; }); - this.consumeContext(UMB_DOCUMENT_STORE_CONTEXT_TOKEN, (documentStore) => { + this.consumeContext(UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN, (documentStore) => { this._documentStore = documentStore; }); } 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 dd544de291..6021b6d817 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,4 @@ -import { STORE_ALIAS } from '../document.store'; +import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from '../document.detail.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.Documents'; @@ -8,7 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Documents Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index e8256d17cf..4f60b6d583 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -1,6 +1,6 @@ import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; -import { STORE_ALIAS as DOCUMENT_STORE_ALIAS } from '../../../documents/documents/document.store'; -import type { UmbDocumentStore, UmbDocumentStoreItemType } from '../../../documents/documents/document.store'; +import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from '../document.detail.store'; +import type { UmbDocumentDetailStore } from '../document.detail.store'; import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; import type { DocumentDetails } from '@umbraco-cms/models'; import { appendToFrozenArray } from '@umbraco-cms/observable-api'; @@ -32,20 +32,19 @@ const DefaultDocumentData = { name: '', }, ], -} as UmbDocumentStoreItemType; +} as DocumentDetails; -export class UmbWorkspaceDocumentContext extends UmbWorkspaceContentContext { +export class UmbWorkspaceDocumentContext extends UmbWorkspaceContentContext { constructor(host: UmbControllerHostInterface) { - super(host, DefaultDocumentData, DOCUMENT_STORE_ALIAS, 'document'); + super(host, DefaultDocumentData, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'document'); } public setPropertyValue(alias: string, value: unknown) { // TODO: make sure to check that we have a details model? otherwise fail? 8This can be relevant if we use the same context for tree actions? - //if(isDocumentDetails(data)) { ... } const entry = {alias: alias, value: value}; - const newDataSet = appendToFrozenArray((this._data.getValue() as DocumentDetails).data, entry, x => x.alias === alias); + const newDataSet = appendToFrozenArray((this._data.getValue() as DocumentDetails).data, entry, (x: any) => x.alias); this.update({data: newDataSet}); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.detail.store.ts new file mode 100644 index 0000000000..a0ff1cd408 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.detail.store.ts @@ -0,0 +1,59 @@ +import type { DataTypeDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTypeDetailStore'); + + +/** + * @export + * @class UmbMediaTypeDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Media Types + */ +export class UmbMediaTypeDetailStore extends UmbStoreBase { + + + private _data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbMediaTypesStore + */ + getByKey(key: string) { + return null as any; + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a Data Type. + * @param {Array} dataTypes + * @memberof UmbMediaTypesStore + * @return {*} {Promise} + */ + save(data: DataTypeDetails[]) { + return null as any; + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Media Type. + * @param {string[]} keys + * @memberof UmbMediaTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + this._data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.store.ts deleted file mode 100644 index 5bcaef4730..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.store.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbNodeStoreBase } from '@umbraco-cms/store'; -import { MediaTypeResource, FolderTreeItem } from '@umbraco-cms/backend-api'; -import type { MediaTypeDetails } from '@umbraco-cms/models'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export type UmbMediaTypeStoreItemType = MediaTypeDetails | FolderTreeItem; - -export const STORE_ALIAS = 'UmbMediaTypeStore'; - -/** - * @export - * @class UmbMediaTypeStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Media Types - */ -export class UmbMediaTypeStore extends UmbNodeStoreBase { - public readonly storeAlias = STORE_ALIAS; - - /** - * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. - * @param {string} key - * @return {*} {(Observable)} - * @memberof UmbMediaTypesStore - */ - getByKey(key: string): Observable { - // TODO: use backend cli when available. - /* - fetch(`/umbraco/backoffice/media-type/details/${key}`) - .then((res) => res.json()) - .then((data) => { - this.updateItems(data); - }); - - return this.items.pipe(map((mediaTypes) => mediaTypes.find((mediaType) => mediaType.key === key && isMediaTypeDetails(mediaType)) as UmbMediaTypeStoreItemType || null)); - */ - return null as any; - } - - /** - * @description - Save a Data Type. - * @param {Array} mediaTypes - * @memberof UmbMediaTypesStore - * @return {*} {Promise} - */ - async save(mediaTypes: Array): Promise { - // TODO: use backend cli when available. - /* - try { - const res = await fetch('/umbraco/backoffice/media-type/save', { - method: 'POST', - body: JSON.stringify(mediaTypes), - headers: { - 'Content-Type': 'application/json', - }, - }); - const json = await res.json(); - this.updateItems(json); - } catch (error) { - console.error('Save Data Type error', error); - } - */ - return null as any; - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, MediaTypeResource.getTreeMediaTypeRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } - - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - MediaTypeResource.getTreeMediaTypeChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key))); - } -} - -export const UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts new file mode 100644 index 0000000000..cb0fa4f758 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts @@ -0,0 +1,70 @@ +import { FolderTreeItem, MediaTypeResource } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTypeTreeStore'); + + +/** + * @export + * @class UmbMediaTypeTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Media Types + */ +export class UmbMediaTypeTreeStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + + getTreeRoot() { + tryExecuteAndNotify(this._host, MediaTypeResource.getTreeMediaTypeRoot({})).then(({ data }) => { + if (data) { + this.#data.append(data.items); + } + }); + + return createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null)); + } + + getTreeItemChildren(key: string){ + tryExecuteAndNotify( + this._host, + MediaTypeResource.getTreeMediaTypeChildren({ + parentKey: key, + }) + ).then(({ data }) => { + if (data) { + 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, + MediaTypeResource.getTreeMediaTypeItem({ + 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/media/media-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts index 54a406aca4..d83d3c5967 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../media-type.store'; +import { UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from '../media-type.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { @@ -6,7 +6,7 @@ const tree: ManifestTree = { alias: 'Umb.Tree.MediaTypes', name: 'Media Types Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.detail.store.ts new file mode 100644 index 0000000000..81e777d384 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.detail.store.ts @@ -0,0 +1,81 @@ +import type { DocumentDetails, MediaDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase, UmbContentStore } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentDetailStore'); + + +/** + * @export + * @class UmbMediaStore + * @extends {UmbStoreBase} + * @description - Data Store for Media + */ +export class UmbMediaDetailStore extends UmbStoreBase implements UmbContentStore { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/media/details/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + }); + + return createObservablePart(this.#data, (documents) => + documents.find((document) => document.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + save(data: MediaDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/media/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this.#data.append(data); + }); + } + + // TODO: how do we handle trashed items? + // TODO: How do we make trash available on details and tree store? + async trash(keys: Array) { + // TODO: use backend cli when available. + const res = await fetch('/umbraco/management/api/v1/media/trash', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await res.json(); + this.#data.append(data); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.store.ts deleted file mode 100644 index 61db13cd6e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.store.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import type { MediaDetails } from '@umbraco-cms/models'; -import { ContentTreeItem, MediaResource } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -const isMediaDetails = (media: UmbMediaStoreItemType): media is MediaDetails => { - return (media as MediaDetails).data !== undefined; -}; - -// TODO: stop using ContentTreeItem. -export type UmbMediaStoreItemType = MediaDetails | ContentTreeItem; - -export const STORE_ALIAS = 'UmbMediaStore'; - -/** - * @export - * @class UmbMediaStore - * @extends {UmbMediaStoreBase} - * @description - Data Store for Media - */ -export class UmbMediaStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - // fetch from server and update store - fetch(`/umbraco/management/api/v1/media/details/${key}`) - .then((res) => res.json()) - .then((data) => { - this.updateItems(data); - }); - - return this.items.pipe( - map((media) => (media.find((media) => media.key === key && isMediaDetails(media)) as MediaDetails) || null) - ); - } - - // TODO: make sure UI somehow can follow the status of this action. - save(data: MediaDetails[]): Promise { - // fetch from server and update store - // TODO: use Fetcher API. - let body: string; - - try { - body = JSON.stringify(data); - } catch (error) { - console.error(error); - return Promise.reject(); - } - - // TODO: Use node type to hit the right API, or have a general Node API? - return fetch('/umbraco/management/api/v1/media/save', { - method: 'POST', - body: body, - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((res) => res.json()) - .then((data: Array) => { - this.updateItems(data); - }); - } - - // TODO: how do we handle trashed items? - async trash(keys: Array) { - // fetch from server and update store - // TODO: Use node type to hit the right API, or have a general Node API? - const res = await fetch('/umbraco/management/api/v1/media/trash', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - const data = await res.json(); - this.updateItems(data); - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, MediaResource.getTreeMediaRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null && !item.isTrashed))); - } - - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - MediaResource.getTreeMediaChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key && !item.isTrashed))); - } -} - -export const UMB_MEDIA_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.tree.store.ts new file mode 100644 index 0000000000..adc6467b25 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.tree.store.ts @@ -0,0 +1,95 @@ +import type { Observable } from 'rxjs'; +import { MediaResource, ContentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTreeStore'); + +// TODO: Stop using ContentTreeItem +type MediaTreeItem = ContentTreeItem; + +/** + * @export + * @class UmbMediaTreeStore + * @extends {UmbStoreBase} + * @description - Data Store for Media + */ +export class UmbMediaTreeStore extends UmbStoreBase { + + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: how do we handle trashed items? + // TODO: How do we make trash available on details and tree store? + async trash(keys: Array) { + // TODO: use backend cli when available. + const res = await fetch('/umbraco/management/api/v1/media/trash', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await res.json(); + this.#data.append(data); + } + + getTreeRoot(): Observable> { + tryExecuteAndNotify(this._host, MediaResource.getTreeMediaRoot({})).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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); + } + + getTreeItemChildren(key: string): Observable> { + tryExecuteAndNotify( + this._host, + MediaResource.getTreeMediaChildren({ + 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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); + } + + getTreeItems(keys: Array): Observable> { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + MediaResource.getTreeMediaItem({ + 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/media/media/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts index 5160b0f9cf..9ab486dfda 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../media.store'; +import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from '../media.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.Media'; @@ -8,7 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Media Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts index 0814d0f8e9..91da78515e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts @@ -4,7 +4,7 @@ import type { ManifestWorkspaceView, ManifestWorkspaceViewCollection, } from '@umbraco-cms/models'; -import { STORE_ALIAS } from '../media.store'; +import { UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from '../media.detail.store'; const workspace: ManifestWorkspace = { type: 'workspace', @@ -59,7 +59,7 @@ const workspaceViewCollections: Array = [ pathname: 'collection', icon: 'umb:grid', entityType: 'media', - storeAlias: STORE_ALIAS, + storeAlias: UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString(), }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts index 490ad41680..c724271abb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts @@ -1,10 +1,10 @@ import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; import { - UmbMediaStore, - UmbMediaStoreItemType, - UMB_MEDIA_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/media/media/media.store'; + UmbMediaDetailStore, + UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN, +} from 'src/backoffice/media/media/media.detail.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { MediaDetails } from '@umbraco-cms/models'; const DefaultMediaData = { key: '', @@ -33,11 +33,11 @@ const DefaultMediaData = { name: '', }, ], -} as UmbMediaStoreItemType; +} as MediaDetails; -export class UmbWorkspaceMediaContext extends UmbWorkspaceContentContext { +export class UmbWorkspaceMediaContext extends UmbWorkspaceContentContext { constructor(host: UmbControllerHostInterface) { - super(host, DefaultMediaData, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString(), 'media'); + super(host, DefaultMediaData, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'media'); } public setPropertyValue(alias: string, value: unknown) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts index b6ae3a143c..81c1dbd762 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from './media/media.store'; +import { UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from './media/media.detail.store'; import type { ManifestDashboardCollection, ManifestSection } from '@umbraco-cms/models'; const sectionAlias = 'Umb.Section.Media'; @@ -25,7 +25,7 @@ const dashboards: Array = [ sections: [sectionAlias], pathname: 'media-management', entityType: 'media', - storeAlias: STORE_ALIAS, + storeAlias: UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString(), }, }, ]; 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 new file mode 100644 index 0000000000..d18fecc6e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.details.store.ts @@ -0,0 +1,34 @@ +import { Observable } from 'rxjs'; +import type { MemberGroupDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UniqueArrayBehaviorSubject } 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 + * @class UmbMemberGroupStore + * @extends {UmbStoreBase} + * @description - Data Store for Member Groups + */ +export class UmbMemberGroupStore extends UmbStoreBase { + + + #groups = new UniqueArrayBehaviorSubject([], x => x.key); + public groups = this.#groups.asObservable(); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString()); + } + + getByKey(key: string): Observable { + return null as any; + } + + async save(mediaTypes: Array): Promise { + return null as any; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.store.ts deleted file mode 100644 index 23b877768d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/member-group.store.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbNodeStoreBase } from '@umbraco-cms/store'; -import { EntityTreeItem, MemberGroupResource } from '@umbraco-cms/backend-api'; -import type { MemberGroupDetails } from '@umbraco-cms/models'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export type UmbMemberGroupStoreItemType = MemberGroupDetails | EntityTreeItem; - -export const STORE_ALIAS = 'UmbMemberGroupStore'; - -/** - * @export - * @class UmbMemberGroupStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Member Groups - */ -export class UmbMemberGroupStore extends UmbNodeStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - return null as any; - } - - async save(mediaTypes: Array): Promise { - return null as any; - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, MemberGroupResource.getTreeMemberGroupRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } -} - -export const UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); 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 45b2878951..8f65257d9a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../member-group.store'; +import { UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN } from '../member-group.details.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.MemberGroups'; @@ -8,7 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Member Groups Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.detail.store.ts new file mode 100644 index 0000000000..a312141aaa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.detail.store.ts @@ -0,0 +1,61 @@ +import type { MemberTypeDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTypeDetailStore'); + + +/** + * @export + * @class UmbMemberTypeDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Member Types + */ +export class UmbMemberTypeDetailStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbMemberTypesStore + */ + getByKey(key: string) { + return null as any; + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a Data Type. + * @param {Array} memberTypes + * @memberof UmbMemberTypesStore + * @return {*} {Promise} + */ + save(data: MemberTypeDetails[]) { + return null as any; + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbMemberTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + return null as any; + + this.#data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.store.ts deleted file mode 100644 index 8d826d9e8b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.store.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import { MemberTypeResource, EntityTreeItem } from '@umbraco-cms/backend-api'; -import type { MemberTypeDetails } from '@umbraco-cms/models'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export type UmbMemberTypeStoreItemType = MemberTypeDetails | EntityTreeItem; - -export const STORE_ALIAS = 'UmbMemberTypeStore'; - -/** - * @export - * @class UmbMemberTypeStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Member Types - */ -export class UmbMemberTypeStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; - - getByKey(key: string): Observable { - return null as any; - } - - async save(mediaTypes: Array): Promise { - return null as any; - } - - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, MemberTypeResource.getTreeMemberTypeRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } -} - -export const UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.tree.store.ts new file mode 100644 index 0000000000..75c5742769 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/member-type.tree.store.ts @@ -0,0 +1,96 @@ +import { EntityTreeItem, MemberTypeResource, } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTypeTreeStore'); + + +/** + * @export + * @class UmbMemberTypeTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Member Types + */ +export class UmbMemberTypeTreeStore extends UmbStoreBase { + + + // TODO: use the right type here: + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbMemberTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/member-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, MemberTypeResource.getTreeMemberTypeRoot({})).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, + MemberTypeResource.getTreeMemberTypeItem({ + 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-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts index b6607ad3e4..fd0030ad39 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../member-type.store'; +import { UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN } from '../member-type.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.MemberTypes'; @@ -8,7 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Member Types Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.detail.store.ts new file mode 100644 index 0000000000..fe8413aba0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.detail.store.ts @@ -0,0 +1,98 @@ +import type { DataTypeDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDataTypeDetailStore'); + + +/** + * @export + * @class UmbDataTypeDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Data Types + */ +export class UmbDataTypeDetailStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbDataTypesStore + */ + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/document/data-type/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + }); + + return createObservablePart(this.#data, (documents) => + documents.find((document) => document.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a Data Type. + * @param {Array} dataTypes + * @memberof UmbDataTypesStore + * @return {*} {Promise} + */ + save(data: DataTypeDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/data-type/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this.#data.append(data); + }); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDataTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/data-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.store.ts deleted file mode 100644 index 43bfee78f9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.store.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import type { DataTypeDetails } from '@umbraco-cms/models'; -import { DataTypeResource, FolderTreeItem } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -const isDataTypeDetails = (dataType: DataTypeDetails | FolderTreeItem): dataType is DataTypeDetails => { - return (dataType as DataTypeDetails).data !== undefined; -}; - -// TODO: can we make is easy to reuse store methods across different stores? - -export type UmbDataTypeStoreItemType = DataTypeDetails | FolderTreeItem; - -// TODO: research how we write names of global consts. -export const STORE_ALIAS = 'UmbDataTypeStore'; - -/** - * @export - * @class UmbDataTypesStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Data Types - */ -export class UmbDataTypeStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; - - /** - * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. - * @param {string} key - * @return {*} {(Observable)} - * @memberof UmbDataTypesStore - */ - getByKey(key: string): Observable { - // TODO: use backend cli when available. - fetch(`/umbraco/backoffice/data-type/details/${key}`) - .then((res) => res.json()) - .then((data) => { - this.updateItems(data); - }); - - return this.items.pipe( - map( - (dataTypes) => - (dataTypes.find((dataType) => dataType.key === key && isDataTypeDetails(dataType)) as DataTypeDetails) || null - ) - ); - } - - /** - * @description - Save a Data Type. - * @param {Array} dataTypes - * @memberof UmbDataTypesStore - * @return {*} {Promise} - */ - async save(dataTypes: Array): Promise { - // TODO: use backend cli when available. - try { - const res = await fetch('/umbraco/backoffice/data-type/save', { - method: 'POST', - body: JSON.stringify(dataTypes), - headers: { - 'Content-Type': 'application/json', - }, - }); - const json = await res.json(); - this.updateItems(json); - } catch (error) { - console.error('Save Data Type error', error); - } - } - - /** - * @description - Delete a Data Type. - * @param {string[]} keys - * @memberof UmbDataTypesStore - * @return {*} {Promise} - */ - async deleteItems(keys: string[]): Promise { - // TODO: use backend cli when available. - await fetch('/umbraco/backoffice/data-type/delete', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.deleteItems(keys); - } - - /** - * @description - Get the root of the tree. - * @return {*} {Observable>} - * @memberof UmbDataTypesStore - */ - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, DataTypeResource.getTreeDataTypeRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } - - /** - * @description - Get the children of a tree item. - * @param {string} key - * @return {*} {Observable>} - * @memberof UmbDataTypesStore - */ - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - DataTypeResource.getTreeDataTypeChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key))); - } -} - -export const UMB_DATA_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.tree.store.ts new file mode 100644 index 0000000000..0258d53a5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/data-type.tree.store.ts @@ -0,0 +1,96 @@ +import { DataTypeResource, DocumentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDataTypeTreeStore'); + + +/** + * @export + * @class UmbDataTypeTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Data Types + */ +export class UmbDataTypeTreeStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDataTypesStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/data-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, DataTypeResource.getTreeDataTypeRoot({})).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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); + } + + getTreeItemChildren(key: string) { + tryExecuteAndNotify( + this._host, + DataTypeResource.getTreeDataTypeChildren({ + 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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); + } + + getTreeItems(keys: Array) { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + DataTypeResource.getTreeDataTypeItem({ + 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/settings/data-types/tree/actions/delete/action-data-type-delete.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/actions/delete/action-data-type-delete.element.ts index 612ba06807..3d80fb6313 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/actions/delete/action-data-type-delete.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/actions/delete/action-data-type-delete.element.ts @@ -2,7 +2,8 @@ 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 { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from '../../../data-type.store'; +import { UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from '../../../data-type.detail.store'; +import type { UmbDataTypeDetailStore } from '../../../data-type.detail.store'; import UmbTreeItemActionElement from '../../../../../shared/components/tree/action/tree-item-action.element'; @customElement('umb-tree-action-data-type-delete') @@ -10,7 +11,7 @@ export default class UmbTreeActionDataTypeDeleteElement extends UmbTreeItemActio static styles = [UUITextStyles, css``]; private _modalService?: UmbModalService; - private _dataTypeStore?: UmbDataTypeStore; + private _dataTypeStore?: UmbDataTypeDetailStore; connectedCallback(): void { super.connectedCallback(); @@ -19,7 +20,7 @@ export default class UmbTreeActionDataTypeDeleteElement extends UmbTreeItemActio this._modalService = modalService; }); - this.consumeContext(UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, (dataTypeStore) => { + this.consumeContext(UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN, (dataTypeStore) => { this._dataTypeStore = dataTypeStore; }); } @@ -34,7 +35,7 @@ export default class UmbTreeActionDataTypeDeleteElement extends UmbTreeItemActio modalHandler?.onClose().then(({ confirmed }: any) => { if (confirmed && this._treeContextMenuService && this._dataTypeStore && this._activeTreeItem) { - this._dataTypeStore?.deleteItems([this._activeTreeItem.key]); + this._dataTypeStore?.delete([this._activeTreeItem.key]); this._treeContextMenuService.close(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts index 488465a362..a73a6c4236 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../data-type.store'; +import { UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from '../data-type.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const tree: ManifestTree = { @@ -7,7 +7,7 @@ const tree: ManifestTree = { name: 'Data Types Tree', weight: 100, meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts index 678d491b39..de3d6619fd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts @@ -1,10 +1,7 @@ import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; -import { - UmbDataTypeStore, - UmbDataTypeStoreItemType, - UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/settings/data-types/data-type.store'; -import type { DataTypeDetails } from '@umbraco-cms/models'; +import { UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN} from '../../../settings/data-types/data-type.detail.store'; +import type { UmbDataTypeDetailStore} from '../../../settings/data-types/data-type.detail.store'; +import type { DataTypeDetails, DataTypePropertyData } from '@umbraco-cms/models'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { appendToFrozenArray } from '@umbraco-cms/observable-api'; @@ -18,14 +15,14 @@ const DefaultDataTypeData = { propertyEditorModelAlias: '', propertyEditorUIAlias: '', data: [], -} as UmbDataTypeStoreItemType; +} as DataTypeDetails; export class UmbWorkspaceDataTypeContext extends UmbWorkspaceContentContext< - UmbDataTypeStoreItemType, - UmbDataTypeStore + DataTypeDetails, + UmbDataTypeDetailStore > { constructor(host: UmbControllerHostInterface) { - super(host, DefaultDataTypeData, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN.toString(), 'dataType'); + super(host, DefaultDataTypeData, UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString(), 'dataType'); } public setPropertyValue(alias: string, value: unknown) { @@ -35,7 +32,7 @@ export class UmbWorkspaceDataTypeContext extends UmbWorkspaceContentContext< const newDataSet = appendToFrozenArray( (this._data.getValue() as DataTypeDetails).data, entry, - (x) => x.alias === alias + (x: DataTypePropertyData) => x.alias ); this.update({ data: newDataSet }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts index c8a09d48a5..1a9e37abd3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts @@ -1,11 +1,11 @@ import { ContentTreeItem } from '@umbraco-cms/backend-api'; -import { UmbTreeDataStore } from '@umbraco-cms/store'; +import { UmbTreeStore } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextToken, UmbContextConsumerController } from '@umbraco-cms/context-api'; import { UniqueBehaviorSubject, UmbObserverController } from '@umbraco-cms/observable-api'; export class UmbCollectionContext< DataType extends ContentTreeItem, - StoreType extends UmbTreeDataStore = UmbTreeDataStore + StoreType extends UmbTreeStore = UmbTreeStore > { private _host: UmbControllerHostInterface; private _entityKey: string | null; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts index 97719a81ea..8edcf49ed8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/dashboards/dashboard-collection.element.ts @@ -3,12 +3,13 @@ import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import '../collection.element'; import { ifDefined } from 'lit-html/directives/if-defined.js'; -import { UmbMediaStore, UmbMediaStoreItemType } from 'src/backoffice/media/media/media.store'; +import { UmbMediaTreeStore } from '../../../media/media/media.tree.store'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN, -} from 'src/backoffice/shared/collection/collection.context'; +} from '../../../shared/collection/collection.context'; import type { ManifestDashboardCollection } from '@umbraco-cms/models'; +import type { FolderTreeItem } from '@umbraco-cms/backend-api'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-dashboard-collection') @@ -26,7 +27,8 @@ export class UmbDashboardCollectionElement extends UmbLitElement { `, ]; - private _collectionContext?: UmbCollectionContext; + // TODO: Use the right type here: + private _collectionContext?: UmbCollectionContext; public manifest!: ManifestDashboardCollection; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/content-property/content-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/content-property/content-property.element.ts index 7ade9d09a1..744d5febe0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/content-property/content-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/content-property/content-property.element.ts @@ -3,8 +3,9 @@ import { css, html } from 'lit'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { customElement, property, state } from 'lit/decorators.js'; -import { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from '../../../settings/data-types/data-type.store'; -import type { ContentProperty, DataTypeDetails } from '@umbraco-cms/models'; +import { UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from '../../../settings/data-types/data-type.detail.store'; +import type { UmbDataTypeDetailStore } from '../../../settings/data-types/data-type.detail.store'; +import type { ContentProperty, DataTypeDetails, DataTypePropertyData } from '@umbraco-cms/models'; import '../workspace-property/workspace-property.element'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -42,15 +43,15 @@ export class UmbContentPropertyElement extends UmbLitElement { private _propertyEditorUIAlias?: string; @state() - private _dataTypeData?: any; + private _dataTypeData: DataTypePropertyData[] = []; - private _dataTypeStore?: UmbDataTypeStore; + private _dataTypeStore?: UmbDataTypeDetailStore; private _dataTypeObserver?: UmbObserverController; constructor() { super(); - this.consumeContext(UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, (instance) => { + this.consumeContext(UMB_DATA_TYPE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { this._dataTypeStore = instance; this._observeDataType(this._property?.dataTypeKey); }); @@ -62,7 +63,7 @@ export class UmbContentPropertyElement extends UmbLitElement { this._dataTypeObserver?.destroy(); if (dataTypeKey) { this._dataTypeObserver = this.observe(this._dataTypeStore.getByKey(dataTypeKey), (dataType) => { - this._dataTypeData = dataType?.data; + this._dataTypeData = dataType?.data || []; this._propertyEditorUIAlias = dataType?.propertyEditorUIAlias || undefined; }); } 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 52966c7b57..080c1287b3 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 @@ -3,10 +3,11 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from '../../../documents/documents/document.store'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; -import type { FolderTreeItem } from '@umbraco-cms/backend-api'; +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 { UmbLitElement } from '@umbraco-cms/element'; +import type { DocumentTreeItem, FolderTreeItem } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; @customElement('umb-input-document-picker') @@ -74,10 +75,10 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen } @state() - private _items?: Array; + private _items?: Array; private _modalService?: UmbModalService; - private _documentStore?: UmbDocumentStore; + private _documentStore?: UmbDocumentTreeStore; private _pickedItemsObserver?: UmbObserverController; constructor() { @@ -94,7 +95,7 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen () => !!this.max && this._selectedKeys.length > this.max ); - this.consumeContext(UMB_DOCUMENT_STORE_CONTEXT_TOKEN, (instance) => { + this.consumeContext(UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN, (instance) => { this._documentStore = instance; this._observePickedDocuments(); }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts index 479661019b..86053ffd54 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts @@ -3,6 +3,7 @@ import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbInputListBase } from '../input-list-base/input-list-base'; import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../../users/user-groups/user-group.store'; + import type { UserGroupEntity } from '@umbraco-cms/models'; @customElement('umb-input-user-group') diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts index a599227afe..e5b4af939d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts @@ -1,6 +1,6 @@ import { BehaviorSubject } from 'rxjs'; import type { Entity, ManifestSection, ManifestSectionView, ManifestTree } from '@umbraco-cms/models'; -import { UniqueBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UniqueObjectBehaviorSubject } from '@umbraco-cms/observable-api'; import { UmbContextToken } from '@umbraco-cms/context-api'; export class UmbSectionContext { @@ -12,7 +12,7 @@ export class UmbSectionContext { public readonly activeTree = this._activeTree.asObservable(); // TODO: what is the best context to put this in? - private _activeTreeItem = new UniqueBehaviorSubject(undefined); + private _activeTreeItem = new UniqueObjectBehaviorSubject(undefined); public readonly activeTreeItem = this._activeTreeItem.asObservable(); // TODO: what is the best context to put this in? @@ -20,7 +20,7 @@ export class UmbSectionContext { public readonly activeView = this._activeView.asObservable(); constructor(sectionManifest: ManifestSection) { - this.#manifest = new UniqueBehaviorSubject(sectionManifest); + this.#manifest = new BehaviorSubject(sectionManifest); this.manifest = this.#manifest.asObservable(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts index 38a900d5a4..c808b0203a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.element.ts @@ -11,7 +11,7 @@ import { UMB_TREE_CONTEXT_MENU_SERVICE_CONTEXT_TOKEN, } from './context-menu/tree-context-menu.service'; import type { Entity } from '@umbraco-cms/models'; -import type { UmbTreeDataStore } from '@umbraco-cms/store'; +import type { UmbTreeStore } from '@umbraco-cms/store'; import { UmbLitElement } from '@umbraco-cms/element'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; @@ -68,7 +68,7 @@ export class UmbTreeItem extends UmbLitElement { private _hasActions = false; private _treeContext?: UmbTreeContextBase; - private _store?: UmbTreeDataStore; + private _store?: UmbTreeStore; private _sectionContext?: UmbSectionContext; private _treeContextMenuService?: UmbTreeContextMenuService; @@ -81,7 +81,7 @@ export class UmbTreeItem extends UmbLitElement { this._observeIsSelected(); }); - this.consumeContext('umbStore', (store: UmbTreeDataStore) => { + this.consumeContext('umbStore', (store: UmbTreeStore) => { this._store = store; }); 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 5bd772e25e..ecb978bd32 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 @@ -5,7 +5,7 @@ import { repeat } from 'lit-html/directives/repeat.js'; import { UmbTreeContextBase } from './tree.context'; import type { Entity, ManifestTree } from '@umbraco-cms/models'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; -import { UmbTreeDataStore } from '@umbraco-cms/store'; +import { UmbTreeStore } from '@umbraco-cms/store'; import { UmbLitElement } from '@umbraco-cms/element'; import './tree-item.element'; @@ -66,7 +66,7 @@ export class UmbTreeElement extends UmbLitElement { private _loading = true; private _treeContext?: UmbTreeContextBase; - private _store?: UmbTreeDataStore; + private _store?: UmbTreeStore; connectedCallback(): void { super.connectedCallback(); @@ -108,7 +108,7 @@ export class UmbTreeElement extends UmbLitElement { if (!this._tree?.meta.storeAlias) return; - this.consumeContext(this._tree.meta.storeAlias, (store: UmbTreeDataStore) => { + this.consumeContext(this._tree.meta.storeAlias, (store: UmbTreeStore) => { this._store = store; this.provideContext('umbStore', store); }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts index adc7588e89..e86656b85c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts @@ -4,12 +4,8 @@ import { customElement, property, state } from 'lit/decorators.js'; import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { distinctUntilChanged } from 'rxjs'; import type { UmbWorkspaceContentContext } from '../workspace/workspace-content/workspace-content.context'; -import type { DocumentDetails, MediaDetails } from '@umbraco-cms/models'; - -import type { UmbNodeStoreBase } from '@umbraco-cms/store'; import { UmbLitElement } from '@umbraco-cms/element'; - -type ContentTypeTypes = DocumentDetails | MediaDetails; +import type { ContentTreeItem } from '@umbraco-cms/backend-api'; @customElement('umb-variant-selector') export class UmbVariantSelectorElement extends UmbLitElement { @@ -45,17 +41,17 @@ export class UmbVariantSelectorElement extends UmbLitElement { @property() alias!: string; - // TODO: use a more specific type here: + // TODO: use a more specific type here, something with variants. @state() - _content?: ContentTypeTypes; + _content?: ContentTreeItem; - private _workspaceContext?: UmbWorkspaceContentContext>; + private _workspaceContext?: UmbWorkspaceContentContext; constructor() { super(); // TODO: Figure out how to get the magic string for the workspace context. - this.consumeContext>>( + this.consumeContext( 'umbWorkspaceContext', (instance) => { this._workspaceContext = instance; @@ -99,7 +95,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { ${ - this._content && this._content.variants?.length > 0 + this._content && (this._content as any).variants?.length > 0 ? html`
@@ -113,7 +109,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { ${ - this._content && this._content.variants?.length > 0 + this._content && (this._content as any).variants?.length > 0 ? html`
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts index 882dc1bdf9..255b7eb11b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/collection/workspace-view-collection.element.ts @@ -3,15 +3,16 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { UmbWorkspaceContentContext } from '../../workspace-content.context'; +import { UmbMediaTreeStore } from '../../../../../../media/media/media.tree.store'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN, -} from 'src/backoffice/shared/collection/collection.context'; -import { UmbMediaStore, UmbMediaStoreItemType } from 'src/backoffice/media/media/media.store'; +} from '../../../../../../shared/collection/collection.context'; import '../../../../../../shared/components/content-property/content-property.element'; import '../../../../../../shared/collection/dashboards/dashboard-collection.element'; import { UmbLitElement } from '@umbraco-cms/element'; +import { FolderTreeItem } from '@umbraco-cms/backend-api'; @customElement('umb-workspace-view-collection') export class UmbWorkspaceViewCollectionElement extends UmbLitElement { @@ -27,7 +28,7 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement { private _workspaceContext?: UmbWorkspaceContentContext; - private _collectionContext?: UmbCollectionContext; + private _collectionContext?: UmbCollectionContext; constructor() { super(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts index 9e12012e43..3a494630a3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts @@ -1,20 +1,16 @@ import { v4 as uuidv4 } from 'uuid'; -import { UniqueBehaviorSubject, UmbObserverController, createObservablePart } from '@umbraco-cms/observable-api'; -import { - UmbNotificationDefaultData, - UmbNotificationService, - UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, -} from '@umbraco-cms/notification'; -import { UmbNodeStoreBase } from '@umbraco-cms/store'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, UmbNotificationDefaultData } from '@umbraco-cms/notification'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/context-api'; -import { EntityTreeItem } from '@umbraco-cms/backend-api'; +import { UniqueBehaviorSubject, UmbObserverController, createObservablePart } from '@umbraco-cms/observable-api'; +import { UmbContentStore } from '@umbraco-cms/store'; +import type { ContentTreeItem } from '@umbraco-cms/backend-api'; // TODO: Consider if its right to have this many class-inheritance of WorkspaceContext // TODO: Could we extract this code into a 'Manager' of its own, which will be instantiated by the concrete Workspace Context. This will be more transparent and 'reuseable' export abstract class UmbWorkspaceContentContext< - ContentTypeType extends EntityTreeItem = EntityTreeItem, - StoreType extends UmbNodeStoreBase = UmbNodeStoreBase + ContentTypeType extends ContentTreeItem = ContentTreeItem, + StoreType extends UmbContentStore = UmbContentStore > { protected _host: UmbControllerHostInterface; @@ -106,7 +102,7 @@ export abstract class UmbWorkspaceContentContext< // TODO: consider turning this into an abstract so each context implement this them selfs. public save(): Promise { if (!this._store) { - // TODO: more beautiful error: + // TODO: add a more beautiful error: console.error('Could not save cause workspace context has no store.'); return Promise.resolve(); } @@ -122,7 +118,7 @@ export abstract class UmbWorkspaceContentContext< }); } - // TODO: how can we make sure to call this. + // TODO: how can we make sure to call this, we might need to turn this thing into a ContextProvider(extending) for it to call destroy? public destroy(): void { this._data.unsubscribe(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.detail.store.ts new file mode 100644 index 0000000000..598a2f16d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.detail.store.ts @@ -0,0 +1,100 @@ +import type { DictionaryDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { EntityTreeItem } from '@umbraco-cms/backend-api'; + + +export const UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDictionaryDetailStore'); + + +/** + * @export + * @class UmbDictionaryDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Data Types + */ +export class UmbDictionaryDetailStore extends UmbStoreBase { + + + // TODO: use the right type: + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbDictionaryDetailStore + */ + getByKey(key: string) { + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/document/dictionary/${key}`) + .then((res) => res.json()) + .then((data) => { + this.#data.append(data); + }); + + return createObservablePart(this.#data, (documents) => + documents.find((document) => document.key === key) + ); + } + + // TODO: make sure UI somehow can follow the status of this action. + /** + * @description - Save a Dictionary. + * @param {Array} Dictionaries + * @memberof UmbDictionaryDetailStore + * @return {*} {Promise} + */ + save(data: DictionaryDetails[]) { + // fetch from server and update store + // TODO: use Fetcher API. + let body: string; + + try { + body = JSON.stringify(data); + } catch (error) { + console.error(error); + return Promise.reject(); + } + + // TODO: use backend cli when available. + return fetch('/umbraco/management/api/v1/dictionary/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data: Array) => { + this.#data.append(data); + }); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDictionaryDetailStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/dictionary/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.store.ts deleted file mode 100644 index 133a0ed9e9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.store.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import { DictionaryResource, EntityTreeItem } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; - -export const STORE_ALIAS = 'UmbDictionaryStore'; - -/** - * @export - * @class UmbDictionaryStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Dictionary Items. - */ -export class UmbDictionaryStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; - - /** - * @description - Get the root of the tree. - * @return {*} {Observable>} - * @memberof UmbDictionaryStore - */ - getTreeRoot(): Observable> { - tryExecuteAndNotify(this.host, DictionaryResource.getTreeDictionaryRoot({})).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === null))); - } - - /** - * @description - Get the children of a tree item. - * @param {string} key - * @return {*} {Observable>} - * @memberof UmbDataTypesStore - */ - getTreeItemChildren(key: string): Observable> { - tryExecuteAndNotify( - this.host, - DictionaryResource.getTreeDictionaryChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - this.updateItems(data.items); - } - }); - - return this.items.pipe(map((items) => items.filter((item) => item.parentKey === key))); - } -} - -export const UMB_DICTIONARY_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.tree.store.ts new file mode 100644 index 0000000000..f7f8687833 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/dictionary.tree.store.ts @@ -0,0 +1,96 @@ +import { DictionaryResource, DocumentTreeItem } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + + +export const UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDictionaryTreeStore'); + + +/** + * @export + * @class UmbDictionaryTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Data Types + */ +export class UmbDictionaryTreeStore extends UmbStoreBase { + + + #data = new UniqueArrayBehaviorSubject([], (x) => x.key); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + // TODO: How can we avoid having this in both stores? + /** + * @description - Delete a Data Type. + * @param {string[]} keys + * @memberof UmbDictionarysStore + * @return {*} {Promise} + */ + async delete(keys: string[]) { + // TODO: use backend cli when available. + await fetch('/umbraco/backoffice/data-type/delete', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.#data.remove(keys); + } + + getTreeRoot() { + tryExecuteAndNotify(this._host, DictionaryResource.getTreeDictionaryRoot({})).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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); + } + + getTreeItemChildren(key: string) { + tryExecuteAndNotify( + this._host, + DictionaryResource.getTreeDictionaryChildren({ + 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 createObservablePart(this.#data, (items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); + } + + getTreeItems(keys: Array) { + if (keys?.length > 0) { + tryExecuteAndNotify( + this._host, + DictionaryResource.getTreeDictionaryItem({ + 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/translation/dictionary/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts index f399178b43..8ba7f7d41e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts @@ -1,4 +1,4 @@ -import { STORE_ALIAS } from '../dictionary.store'; +import { UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from '../dictionary.tree.store'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; const treeAlias = 'Umb.Tree.Dictionary'; @@ -8,7 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Dictionary Tree', meta: { - storeAlias: STORE_ALIAS, + storeAlias: UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN.toString(), }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts index edbb706351..6c2e68b807 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts @@ -1,67 +1,67 @@ -import { map, Observable } from 'rxjs'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; -import type { UserGroupDetails, UserGroupEntity } from '@umbraco-cms/models'; +import type { UserGroupDetails } from '@umbraco-cms/models'; import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; // TODO: get rid of this type addition & { ... }: export type UmbUserGroupStoreItemType = UserGroupDetails & { users?: Array }; -export const STORE_ALIAS = 'UmbUserGroupStore'; +export const UMB_USER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbUserGroupStore'); /** * @export * @class UmbUserGroupStore - * @extends {UmbDataStoreBase} - * @description - Data Store for Users + * @extends {UmbStoreBase} + * @description - Data Store for User Groups */ -export class UmbUserGroupStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; +export class UmbUserGroupStore extends UmbStoreBase { - getAll(): Observable> { + + #groups = new UniqueArrayBehaviorSubject([], x => x.key); + public groups = this.#groups.asObservable(); + + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_USER_GROUP_STORE_CONTEXT_TOKEN.toString()); + } + + getAll() { // TODO: use Fetcher API. // TODO: only fetch if the data type is not in the store? fetch(`/umbraco/backoffice/user-groups/list/items`) .then((res) => res.json()) .then((data) => { - this.updateItems(data.items); + this.#groups.append(data.items); }); - return this.items; + return this.groups; } - getByKey(key: string): Observable { + getByKey(key: string) { // TODO: use Fetcher API. // TODO: only fetch if the data type is not in the store? fetch(`/umbraco/backoffice/user-groups/details/${key}`) .then((res) => res.json()) .then((data) => { - this.updateItems([data]); + this.#groups.append([data]); }); - return this.items.pipe( - map( - (userGroups: Array) => - userGroups.find((userGroup: UmbUserGroupStoreItemType) => userGroup.key === key) || null - ) - ); + return createObservablePart(this.groups, (userGroups) => userGroups.find(userGroup => userGroup.key === key)); } - getByKeys(keys: Array): Observable> { + getByKeys(keys: Array) { const params = keys.map((key) => `key=${key}`).join('&'); fetch(`/umbraco/backoffice/user-groups/getByKeys?${params}`) .then((res) => res.json()) .then((data) => { - this.updateItems(data); + this.#groups.append(data); }); - return this.items.pipe( - map((items: Array) => - items.filter((node: UmbUserGroupStoreItemType) => keys.includes(node.key)) - ) - ); + return createObservablePart(this.groups, (items) => items.filter(node => keys.includes(node.key))); } - async save(userGroups: Array): Promise { + async save(userGroups: Array) { // TODO: use Fetcher API. // TODO: implement so user group store updates the @@ -80,11 +80,9 @@ export class UmbUserGroupStore extends UmbDataStoreBase(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts index cc3d7bee39..670173fa49 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts @@ -1,72 +1,85 @@ -import { map, Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import type { UserDetails } from '@umbraco-cms/models'; -import { UniqueBehaviorSubject } from '@umbraco-cms/observable-api'; +import { createObservablePart, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { UmbDataStoreBase } from '@umbraco-cms/store'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; export type UmbUserStoreItemType = UserDetails; -export const STORE_ALIAS = 'UmbUserStore'; +export const UMB_USER_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbUserStore'); /** * @export * @class UmbUserStore - * @extends {UmbDataStoreBase} + * @extends {UmbStoreBase} * @description - Data Store for Users */ -export class UmbUserStore extends UmbDataStoreBase { - public readonly storeAlias = STORE_ALIAS; +export class UmbUserStore extends UmbStoreBase { - #totalUsers = new UniqueBehaviorSubject(0); + + #users = new UniqueArrayBehaviorSubject([], x => x.key); + public users = this.#users.asObservable(); + + #totalUsers = new BehaviorSubject(0); public readonly totalUsers = this.#totalUsers.asObservable(); - getAll(): Observable> { + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_USER_STORE_CONTEXT_TOKEN.toString()); + } + + + getAll() { // TODO: use Fetcher API. // TODO: only fetch if the data type is not in the store? fetch(`/umbraco/backoffice/users/list/items`) .then((res) => res.json()) .then((data) => { this.#totalUsers.next(data.total); - this.updateItems(data.items); + this.#users.next(data.items); }); - return this.items; + return this.users; } /** - * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. + * @description - Request a User by key. The User is added to the store and is returned as an Observable. * @param {string} key * @return {*} {(Observable)} * @memberof UmbDataTypeStore */ - getByKey(key: string): Observable { + getByKey(key: string) { // TODO: use Fetcher API. // TODO: only fetch if the data type is not in the store? fetch(`/umbraco/backoffice/users/details/${key}`) .then((res) => res.json()) .then((data) => { - this.updateItems([data]); + this.#users.appendOne(data); }); - return this.items.pipe( - map((items: Array) => items.find((node: UmbUserStoreItemType) => node.key === key) || null) - ); + return createObservablePart(this.#users, (users: Array) => users.find((user: UmbUserStoreItemType) => user.key === key)); } - getByKeys(keys: Array): Observable> { + + /** + * @description - Request Users by keys. + * @param {string} key + * @return {*} {(Observable)} + * @memberof UmbDataTypeStore + */ + getByKeys(keys: Array) { const params = keys.map((key) => `key=${key}`).join('&'); fetch(`/umbraco/backoffice/users/getByKeys?${params}`) .then((res) => res.json()) .then((data) => { - this.updateItems(data); + this.#users.append(data); }); - return this.items.pipe( - map((items: Array) => items.filter((node: UmbUserStoreItemType) => keys.includes(node.key))) - ); + return createObservablePart(this.#users, (users: Array) => users.filter((user: UmbUserStoreItemType) => keys.includes(user.key))); } - getByName(name: string): Observable> { + getByName(name: string) { name = name.trim(); name = name.toLocaleLowerCase(); @@ -74,17 +87,13 @@ export class UmbUserStore extends UmbDataStoreBase { fetch(`/umbraco/backoffice/users/getByName?${params}`) .then((res) => res.json()) .then((data) => { - this.updateItems(data); + this.#users.append(data); }); - return this.items.pipe( - map((items: Array) => - items.filter((node: UserDetails) => node.name.toLocaleLowerCase().includes(name)) - ) - ); + return createObservablePart(this.#users, (users: Array) => users.filter((user: UmbUserStoreItemType) => user.name.toLocaleLowerCase().includes(name))); } - async enableUsers(userKeys: Array): Promise { + async enableUsers(userKeys: Array) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/enable', { @@ -95,19 +104,19 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const enabledKeys = await res.json(); - const storedUsers = this._items.getValue().filter((user) => enabledKeys.includes(user.key)); + const storedUsers = this.#users.getValue().filter((user) => enabledKeys.includes(user.key)); storedUsers.forEach((user) => { user.status = 'enabled'; }); - this.updateItems(storedUsers); + this.#users.append(storedUsers); } catch (error) { console.error('Enable Users failed', error); } } - async updateUserGroup(userKeys: Array, userGroup: string): Promise { + async updateUserGroup(userKeys: Array, userGroup: string) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/updateUserGroup', { @@ -118,7 +127,7 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const enabledKeys = await res.json(); - const storedUsers = this._items.getValue().filter((user) => enabledKeys.includes(user.key)); + const storedUsers = this.#users.getValue().filter((user) => enabledKeys.includes(user.key)); storedUsers.forEach((user) => { if (userKeys.includes(user.key)) { @@ -128,13 +137,13 @@ export class UmbUserStore extends UmbDataStoreBase { } }); - this.updateItems(storedUsers); + this.#users.append(storedUsers); } catch (error) { console.error('Add user group failed', error); } } - async removeUserGroup(userKeys: Array, userGroup: string): Promise { + async removeUserGroup(userKeys: Array, userGroup: string) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/enable', { @@ -145,19 +154,19 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const enabledKeys = await res.json(); - const storedUsers = this._items.getValue().filter((user) => enabledKeys.includes(user.key)); + const storedUsers = this.#users.getValue().filter((user) => enabledKeys.includes(user.key)); storedUsers.forEach((user) => { user.userGroups = user.userGroups.filter((group) => group !== userGroup); }); - this.updateItems(storedUsers); + this.#users.append(storedUsers); } catch (error) { console.error('Remove user group failed', error); } } - async disableUsers(userKeys: Array): Promise { + async disableUsers(userKeys: Array) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/disable', { @@ -168,19 +177,19 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const disabledKeys = await res.json(); - const storedUsers = this._items.getValue().filter((user) => disabledKeys.includes(user.key)); + const storedUsers = this.#users.getValue().filter((user) => disabledKeys.includes(user.key)); storedUsers.forEach((user) => { user.status = 'disabled'; }); - this.updateItems(storedUsers); + this.#users.append(storedUsers); } catch (error) { console.error('Disable Users failed', error); } } - async deleteUsers(userKeys: Array): Promise { + async deleteUsers(userKeys: Array) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/delete', { @@ -191,13 +200,13 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const deletedKeys = await res.json(); - this.deleteItems(deletedKeys); + this.#users.remove(deletedKeys); } catch (error) { console.error('Delete Users failed', error); } } - async save(users: Array): Promise { + async save(users: Array) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/save', { @@ -208,9 +217,9 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const json = await res.json(); - this.updateItems(json); + this.#users.append(json); } catch (error) { - console.error('Save Data Type error', error); + console.error('Save user error', error); } } @@ -219,7 +228,7 @@ export class UmbUserStore extends UmbDataStoreBase { email: string, message: string, userGroups: Array - ): Promise { + ) { // TODO: use Fetcher API. try { const res = await fetch('/umbraco/backoffice/users/invite', { @@ -230,7 +239,7 @@ export class UmbUserStore extends UmbDataStoreBase { }, }); const json = (await res.json()) as UmbUserStoreItemType[]; - this.updateItems(json); + this.#users.append(json); return json[0]; } catch (error) { console.error('Invite user error', error); @@ -279,5 +288,3 @@ export class UmbUserStore extends UmbDataStoreBase { // this.requestUpdate('users'); // } } - -export const UMB_USER_STORE_CONTEXT_TOKEN = new UmbContextToken(STORE_ALIAS); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts index 772f88ed9c..1393c00767 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts @@ -3,7 +3,7 @@ import { UmbUserStore, UmbUserStoreItemType, UMB_USER_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/users/users/user.store'; +} from '../../users/user.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; const DefaultDataTypeData = { diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts index 62fc6559d3..4ef941cf50 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts @@ -2,10 +2,8 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base'; -import { - UMB_USER_GROUP_STORE_CONTEXT_TOKEN, - UmbUserGroupStore, -} from '../../../../backoffice/users/user-groups/user-group.store'; +import { UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../../../backoffice/users/user-groups/user-group.store'; +import type { UmbUserGroupStore } from '../../../../backoffice/users/user-groups/user-group.store'; import type { UserGroupDetails } from '@umbraco-cms/models'; @customElement('umb-picker-layout-user-group')