document store split

This commit is contained in:
Niels Lyngsø
2023-01-23 13:59:42 +01:00
parent ff810ab694
commit b6bf4d18fd
14 changed files with 216 additions and 224 deletions

View File

@@ -18,7 +18,7 @@ import {
} 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 { UmbDocumentStore, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from './documents/documents/document.detail.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';
@@ -67,7 +67,9 @@ 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));
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));

View File

@@ -0,0 +1,83 @@
import { map, Observable } from 'rxjs';
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, UniqueArrayBehaviorSubject } from '@umbraco-cms/observable-api';
import { UmbStoreBase } from '@umbraco-cms/stores/store-base';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export const UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDocumentDetailStore>('UmbDocumentDetailStore');
/**
* @export
* @class UmbDocumentStore
* @extends {UmbStoreBase<DocumentDetails>}
* @description - Data Store for Documents
*/
export class UmbDocumentDetailStore extends UmbStoreBase {
private _data = new UniqueArrayBehaviorSubject<DocumentDetails>([], (a, b) => a.key === b.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN.toString());
}
getByKey(key: string): Observable<DocumentDetails | undefined> {
// 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[]): Promise<void> {
// 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<DocumentDetails>) => {
this._data.append(data);
});
}
// TODO: how do we handle trashed items?
async trash(keys: Array<string>) {
// 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.next(data);
}
}

View File

@@ -1,141 +0,0 @@
import { map, Observable } from 'rxjs';
import { UmbNodeStoreBase } from '../../../core/stores/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<DocumentDetails | DocumentTreeItem>}
* @description - Data Store for Documents
*/
export class UmbDocumentStore extends UmbNodeStoreBase<UmbDocumentStoreItemType> {
public readonly storeAlias = STORE_ALIAS;
getByKey(key: string): Observable<DocumentDetails | undefined> {
// 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<void> {
// 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<DocumentDetails>) => {
this.updateItems(data);
});
}
// TODO: how do we handle trashed items?
async trash(keys: Array<string>) {
// 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<Array<DocumentTreeItem>> {
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<Array<FolderTreeItem>> {
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<string>): Observable<Array<FolderTreeItem>> {
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<UmbDocumentStore>(STORE_ALIAS);

View File

@@ -0,0 +1,91 @@
import type { Observable } from 'rxjs';
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/stores/store-base';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDocumentTreeStore>('UmbDocumentDetailStore');
/**
* @export
* @class UmbDocumentStore
* @extends {UmbStoreBase<DocumentTree>}
* @description - Data Store for Documents
*/
export class UmbDocumentTreeStore extends UmbStoreBase {
private _data = new UniqueArrayBehaviorSubject<DocumentTreeItem>([], (a, b) => a.key === b.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString());
}
// TODO: how do we handle trashed items?
async trash(keys: Array<string>) {
// 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.next(data);
}
getTreeRoot(): Observable<Array<DocumentTreeItem>> {
tryExecuteAndNotify(this._host, DocumentResource.getTreeDocumentRoot({})).then(({ data }) => {
if (data) {
this._data.append(data.items);
}
});
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
// 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<Array<DocumentTreeItem>> {
tryExecuteAndNotify(
this._host,
DocumentResource.getTreeDocumentChildren({
parentKey: key,
})
).then(({ data }) => {
if (data) {
this._data.append(data.items);
}
});
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
// 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<string>): Observable<Array<DocumentTreeItem>> {
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 ?? '')));
}
}

View File

@@ -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;
});
}

View File

@@ -1,4 +1,4 @@
import { STORE_ALIAS } from '../document.store';
import { STORE_ALIAS } from '../document.detail.store';
import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models';
const treeAlias = 'Umb.Tree.Documents';

View File

@@ -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 { STORE_ALIAS as DOCUMENT_STORE_ALIAS } from '../document.detail.store';
import type { UmbDocumentStore, UmbDocumentStoreItemType } 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';

View File

@@ -1,11 +1,11 @@
import { ContentTreeItem } from '@umbraco-cms/backend-api';
import { UmbTreeDataStore } from '@umbraco-cms/stores/store';
import { UmbTreeStore } from '@umbraco-cms/stores/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<DataType> = UmbTreeDataStore<DataType>
StoreType extends UmbTreeStore<DataType> = UmbTreeStore<DataType>
> {
private _host: UmbControllerHostInterface;
private _entityKey: string | null;

View File

@@ -7,7 +7,8 @@ import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from 'src/core/modal
import type { FolderTreeItem } from '@umbraco-cms/backend-api';
import { UmbLitElement } from '@umbraco-cms/element';
import type { UmbObserverController } from '@umbraco-cms/observable-api';
import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from 'src/backoffice/documents/documents/document.store';
import type { UmbDocumentDetailStore } from 'src/backoffice/documents/documents/document.detail.store';
import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from 'src/backoffice/documents/documents/document.detail.store';
@customElement('umb-input-document-picker')
export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElement) {
@@ -77,7 +78,7 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen
private _items?: Array<FolderTreeItem>;
private _modalService?: UmbModalService;
private _documentStore?: UmbDocumentStore;
private _documentStore?: UmbDocumentDetailStore;
private _pickedItemsObserver?: UmbObserverController<FolderTreeItem>;
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_DETAIL_STORE_CONTEXT_TOKEN, (instance) => {
this._documentStore = instance;
this._observePickedDocuments();
});

View File

@@ -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 { UmbTreeDataStore } from '@umbraco-cms/stores/store';
import { UmbTreeStore } from '@umbraco-cms/stores/store';
import { UmbLitElement } from '@umbraco-cms/element';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@@ -68,7 +68,7 @@ export class UmbTreeItem extends UmbLitElement {
private _hasActions = false;
private _treeContext?: UmbTreeContextBase;
private _store?: UmbTreeDataStore<unknown>;
private _store?: UmbTreeStore<unknown>;
private _sectionContext?: UmbSectionContext;
private _treeContextMenuService?: UmbTreeContextMenuService;
@@ -81,7 +81,7 @@ export class UmbTreeItem extends UmbLitElement {
this._observeIsSelected();
});
this.consumeContext('umbStore', (store: UmbTreeDataStore<unknown>) => {
this.consumeContext('umbStore', (store: UmbTreeStore<unknown>) => {
this._store = store;
});

View File

@@ -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-registry';
import { UmbTreeDataStore } from '@umbraco-cms/stores/store';
import { UmbTreeStore } from '@umbraco-cms/stores/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<Entity>;
private _store?: UmbTreeStore<Entity>;
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<Entity>) => {
this.consumeContext(this._tree.meta.storeAlias, (store: UmbTreeStore<Entity>) => {
this._store = store;
this.provideContext('umbStore', store);
});

View File

@@ -49,6 +49,7 @@ export class UniqueArrayBehaviorSubject<T> extends UniqueBehaviorSubject<T[]> {
* ]);
*/
append(entries: T[]) {
// TODO: stop calling appendOne for each but make sure to handle this in one.
entries.forEach(x => this.appendOne(x))
}
}

View File

@@ -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);
}
}

View File

@@ -1,6 +1,4 @@
import type { Observable } from 'rxjs';
import { UmbControllerHostInterface } from '../controller/controller-host.mixin';
import { UniqueBehaviorSubject } from '../observable-api/unique-behavior-subject';
export interface UmbDataStoreIdentifiers {
key?: string;
@@ -9,80 +7,25 @@ export interface UmbDataStoreIdentifiers {
export interface UmbDataStore<T> {
readonly storeAlias: string;
// TODO: is items the right name?
readonly items: Observable<Array<T>>;
updateItems(items: Array<T>): void;
}
export interface UmbTreeDataStore<T> extends UmbDataStore<T> {
export interface UmbTreeStore<T> extends UmbDataStore<T> {
getTreeRoot(): Observable<Array<T>>;
getTreeItemChildren(key: string): Observable<Array<T>>;
}
/**
* @export
* @class UmbDataStoreBase
* @implements {UmbDataStore<T>}
* @template T
* @description - Base class for Data Stores
*/
export abstract class UmbDataStoreBase<T extends UmbDataStoreIdentifiers> implements UmbDataStore<T> {
public abstract readonly storeAlias: string;
protected _items = new UniqueBehaviorSubject(<Array<T>>[]);
public readonly items = this._items.asObservable();
protected host: UmbControllerHostInterface;
constructor(host: UmbControllerHostInterface) {
this.host = host;
}
/**
* @description - Delete items from the store.
* @param {Array<string>} keys
* @memberof UmbDataStoreBase
*/
public deleteItems(keys: Array<string>): 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<T>} items
* @param {keyof T} [compareKey='key']
* @memberof UmbDataStoreBase
*/
public updateItems(items: Array<T>, 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<T>}
* @template T
* @description - Base class for Data Stores
*/
export abstract class UmbNodeStoreBase<T extends UmbDataStoreIdentifiers> extends UmbDataStoreBase<T> {
export interface UmbContentStore<T> extends UmbDataStore<T> {
/**
* @description - Request data by key. The data is added to the store and is returned as an Observable.
* @param {string} key
* @return {*} {(Observable<unknown>)}
* @return {*} {(Observable<T>)}
* @memberof UmbDataStoreBase
*/
abstract getByKey(key: string): Observable<unknown>;
getByKey(key: string): Observable<T>;
/**
* @description - Save data.
@@ -90,5 +33,5 @@ export abstract class UmbNodeStoreBase<T extends UmbDataStoreIdentifiers> extend
* @return {*} {(Promise<void>)}
* @memberof UmbNodeStoreBase
*/
abstract save(data: T[]): Promise<void>;
save(data: T[]): Promise<void>;
}