diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard-collection.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard-collection.models.ts index bb10c6d4f8..b41add8d85 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard-collection.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard-collection.models.ts @@ -10,5 +10,5 @@ export interface MetaDashboardCollection { pathname: string; label?: string; entityType: string; - storeAlias: string; + repositoryAlias: string; } 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 2a60168a8c..b79cb4e537 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -21,8 +21,8 @@ import { UmbMediaTypeDetailStore } from './media/media-types/media-type.detail.s import { UmbMediaTypeTreeStore } from './media/media-types/media-type.tree.store'; import { UmbDocumentDetailStore } from './documents/documents/repository/document.detail.store'; import { UmbDocumentTreeStore } from './documents/documents/repository/document.tree.store'; -import { UmbMediaDetailStore } from './media/media/media.detail.store'; -import { UmbMediaTreeStore } from './media/media/media.tree.store'; +import { UmbMediaDetailStore } from './media/media/repository/media.detail.store'; +import { UmbMediaTreeStore } from './media/media/repository/media.tree.store'; import { UmbMemberTypeDetailStore } from './members/member-types/member-type.detail.store'; import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store'; import { UmbMemberGroupStore } from './members/member-groups/member-group.details.store'; 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 7aa42c0165..6401a366d1 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,3 @@ -import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../repository/document.tree.store'; import { UmbDocumentRepository } from '../repository/document.repository'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; @@ -9,8 +8,7 @@ const tree: ManifestTree = { alias: treeAlias, name: 'Documents Tree', meta: { - storeAlias: UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString(), - repository: UmbDocumentRepository, + repository: UmbDocumentRepository, // TODO: use alias instead of class }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/manifests.ts index 978a3ce42b..32cea87d98 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/manifests.ts @@ -1,9 +1,11 @@ +import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; import { manifests as entityActionsManifests } from './entity-actions/manifests'; export const manifests = [ + ...repositoryManifests, ...sidebarMenuItemManifests, ...treeManifests, ...workspaceManifests, 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 deleted file mode 100644 index 965b43aefa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.detail.store.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { MediaDetails } from '@umbraco-cms/models'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } 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('UmbMediaDetailStore'); - - -/** - * @export - * @class UmbMediaDetailStore - * @extends {UmbStoreBase} - * @description - Data Store for Media - */ -export class UmbMediaDetailStore extends UmbStoreBase implements UmbContentStore { - - - #data = new ArrayState([], (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 this.#data.getObservablePart((documents) => - documents.find((document) => document.key === key) - ); - } - - getScaffold(entityType: string, parentKey: string | null) { - return { - key: '', - name: '', - icon: '', - type: '', - hasChildren: false, - parentKey: '', - isTrashed: false, - properties: [ - { - alias: '', - label: '', - description: '', - dataTypeKey: '', - }, - ], - data: [ - { - alias: '', - value: '', - }, - ] - } as MediaDetails; - } - - // 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.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.tree.store.ts deleted file mode 100644 index bd9b03dfc4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/media.tree.store.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase, UmbTreeStore } 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 -export type MediaTreeItem = ContentTreeItem; - -/** - * @export - * @class UmbMediaTreeStore - * @extends {UmbStoreBase} - * @description - Data Store for Media - */ -export class UmbMediaTreeStore extends UmbStoreBase implements UmbTreeStore { - - #data = new ArrayState([], (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); - } - - async move(keys: Array, destination: string) { - // TODO: use backend cli when available. - const res = await fetch('/umbraco/management/api/v1/media/move', { - method: 'POST', - body: JSON.stringify({ keys, destination }), - 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 this.#data.getObservablePart((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 this.#data.getObservablePart((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 this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/manifests.ts new file mode 100644 index 0000000000..42be462f7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbMediaRepository } from './media.repository'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; + +export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Media'; + +const repository: ManifestRepository = { + type: 'repository', + alias: DOCUMENT_REPOSITORY_ALIAS, + name: 'Media Repository', + class: UmbMediaRepository, +}; + +export const manifests = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts new file mode 100644 index 0000000000..7519f79e8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts @@ -0,0 +1,44 @@ +import type { MediaDetails } from '@umbraco-cms/models'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbMediaDetailStore + * @extends {UmbStoreBase} + * @description - Data Store for Template Details + */ +export class UmbMediaDetailStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbMediaDetailStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbMediaDetailStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UmbMediaDetailStore.name); + } + + /** + * Append a media to the store + * @param {MediaDetails} media + * @memberof UmbMediaDetailStore + */ + append(media: MediaDetails) { + this.#data.append([media]); + } + + /** + * Removes media in the store with the given uniques + * @param {string[]} uniques + * @memberof UmbMediaDetailStore + */ + remove(uniques: string[]) { + this.#data.remove(uniques); + } +} + +export const UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken(UmbMediaDetailStore.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts new file mode 100644 index 0000000000..4eeaaf7f49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts @@ -0,0 +1,227 @@ +import type { RepositoryTreeDataSource } from '../../../../../libs/repository/repository-tree-data-source.interface'; +import { MediaTreeServerDataSource } from './sources/media.tree.server.data'; +import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store'; +import { UmbMediaDetailStore, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from './media.detail.store'; +import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import type { UmbTreeRepository } from 'libs/repository/tree-repository.interface'; +import { UmbDetailRepository } from '@umbraco-cms/repository'; +import type { MediaDetails } from '@umbraco-cms/models'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; + +type ItemDetailType = MediaDetails; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepository { + #init!: Promise; + + #host: UmbControllerHostInterface; + + #treeSource: RepositoryTreeDataSource; + #treeStore?: UmbMediaTreeStore; + + #detailDataSource: UmbMediaDetailServerDataSource; + #detailStore?: UmbMediaDetailStore; + + #notificationService?: UmbNotificationService; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + // TODO: figure out how spin up get the correct data source + this.#treeSource = new MediaTreeServerDataSource(this.#host); + this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { + this.#detailStore = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + }), + ]); + } + + async requestRootTreeItems() { + await this.#init; + + const { data, error } = await this.#treeSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error, asObservable: () => this.#treeStore!.rootItems }; + } + + async requestTreeItemsOf(parentKey: string | null) { + await this.#init; + + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#treeSource.getChildrenOf(parentKey); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentKey) }; + } + + async requestTreeItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#treeSource.getItems(keys); + + return { data, error, asObservable: () => this.#treeStore!.items(keys) }; + } + + async rootTreeItems() { + await this.#init; + return this.#treeStore!.rootItems; + } + + async treeItemsOf(parentKey: string | null) { + await this.#init; + return this.#treeStore!.childrenOf(parentKey); + } + + async treeItems(keys: Array) { + await this.#init; + return this.#treeStore!.items(keys); + } + + // DETAILS: + + async createDetailsScaffold(parentKey: string | null) { + await this.#init; + + if (!parentKey) { + throw new Error('Parent key is missing'); + } + + return this.#detailDataSource.createScaffold(parentKey); + } + + async requestDetails(key: string) { + await this.#init; + + // TODO: should we show a notification if the key is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + const { data, error } = await this.#detailDataSource.get(key); + + if (data) { + this.#detailStore?.append(data); + } + + return { data, error }; + } + + // Could potentially be general methods: + + async createDetail(template: ItemDetailType) { + await this.#init; + + if (!template || !template.key) { + throw new Error('Template is missing'); + } + + const { error } = await this.#detailDataSource.insert(template); + + if (!error) { + const notification = { data: { message: `Media created` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server + this.#detailStore?.append(template); + // TODO: Update tree store with the new item? or ask tree to request the new item? + + return { error }; + } + + async saveDetail(document: ItemDetailType) { + await this.#init; + + if (!document || !document.key) { + throw new Error('Template is missing'); + } + + const { error } = await this.#detailDataSource.update(document); + + if (!error) { + const notification = { data: { message: `Document saved` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server + // Consider notify a workspace if a template is updated in the store while someone is editing it. + this.#detailStore?.append(document); + this.#treeStore?.updateItem(document.key, { name: document.name }); + + // TODO: would be nice to align the stores on methods/methodNames. + + return { error }; + } + + // General: + async delete(key: string) { + await this.#init; + + if (!key) { + throw new Error('Document key is missing'); + } + + const { error } = await this.#detailDataSource.delete(key); + + if (!error) { + const notification = { data: { message: `Document deleted` } }; + this.#notificationService?.peek('positive', notification); + } + + // TODO: we currently don't use the detail store for anything. + // Consider to look up the data before fetching from the server. + // Consider notify a workspace if a template is deleted from the store while someone is editing it. + this.#detailStore?.remove([key]); + this.#treeStore?.removeItem(key); + // TODO: would be nice to align the stores on methods/methodNames. + + return { error }; + } + + async move() { + alert('move'); + } + + async copy() { + alert('copy'); + } + + async sortChildrenOf() { + alert('sort'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts new file mode 100644 index 0000000000..4500f78596 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts @@ -0,0 +1,91 @@ +import { EntityTreeItem } from '@umbraco-cms/backend-api'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbMediaTreeStore + * @extends {UmbStoreBase} + * @description - Tree Data Store for Templates + */ +// TODO: consider if tree store could be turned into a general EntityTreeStore class? +export class UmbMediaTreeStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + /** + * Creates an instance of UmbMediaTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbMediaTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbMediaTreeStore + */ + appendItems(items: Array) { + this.#data.append(items); + } + + /** + * Updates an item in the store + * @param {string} key + * @param {Partial} data + * @memberof UmbMediaTreeStore + */ + updateItem(key: string, data: Partial) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.appendOne({ ...entry, ...data }); + } + } + + /** + * Removes an item from the store + * @param {string} key + * @memberof UmbMediaTreeStore + */ + removeItem(key: string) { + const entries = this.#data.getValue(); + const entry = entries.find((entry) => entry.key === key); + + if (entry) { + this.#data.remove([key]); + } + } + + /** + * An observable to observe the root items + * @memberof UmbMediaTreeStore + */ + rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); + + /** + * Returns an observable to observe the children of a given parent + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbMediaTreeStore + */ + childrenOf(parentKey: string | null) { + return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); + } + + /** + * Returns an observable to observe the items with the given keys + * @param {Array} keys + * @return {*} + * @memberof UmbMediaTreeStore + */ + items(keys: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + } +} + +export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken(UmbMediaTreeStore.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts new file mode 100644 index 0000000000..4532cd112f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts @@ -0,0 +1,202 @@ +import { RepositoryDetailDataSource } from '@umbraco-cms/repository'; +import { ProblemDetails } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import type { MediaDetails } from '@umbraco-cms/models'; + +/** + * A data source for the Template detail that fetches data from the server + * @export + * @class UmbTemplateDetailServerDataSource + * @implements {TemplateDetailDataSource} + */ +export class UmbMediaDetailServerDataSource implements RepositoryDetailDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbMediaDetailServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbMediaDetailServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches a Media with the given key from the server + * @param {string} key + * @return {*} + * @memberof UmbMediaDetailServerDataSource + */ + async get(key: string) { + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + // TODO: use backend cli when available. + fetch(`/umbraco/management/api/v1/media/details/${key}`) + .then((res) => res.json()) + .then((res) => res[0] || undefined) + ); + } + + /** + * Creates a new Media scaffold + * @param {(string | null)} parentKey + * @return {*} + * @memberof UmbMediaDetailServerDataSource + */ + async createScaffold(parentKey: string | null) { + const data: MediaDetails = { + key: '', + name: '', + icon: '', + type: '', + hasChildren: false, + parentKey: parentKey ?? '', + isTrashed: false, + properties: [ + { + alias: '', + label: '', + description: '', + dataTypeKey: '', + }, + ], + data: [ + { + alias: '', + value: '', + }, + ], + variants: [ + { + name: '', + }, + ], + }; + + return { data }; + } + + /** + * Inserts a new Media on the server + * @param {Media} media + * @return {*} + * @memberof UmbMediaDetailServerDataSource + */ + async insert(media: MediaDetails) { + if (!media.key) { + //const error: ProblemDetails = { title: 'Media key is missing' }; + return Promise.reject(); + } + //const payload = { key: media.key, requestBody: media }; + + let body: string; + + try { + body = JSON.stringify(media); + } catch (error) { + console.error(error); + return Promise.reject(); + } + //return tryExecuteAndNotify(this.#host, MediaResource.postMedia(payload)); + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) as any + ); + } + + /** + * Updates a Media on the server + * @param {Media} Media + * @return {*} + * @memberof UmbMediaDetailServerDataSource + */ + // TODO: Error mistake in this: + async update(media: MediaDetails) { + if (!media.key) { + const error: ProblemDetails = { title: 'Media key is missing' }; + return { error }; + } + //const payload = { key: media.key, requestBody: media }; + + let body: string; + + try { + body = JSON.stringify(media); + } catch (error) { + const myError: ProblemDetails = { title: 'JSON could not parse' }; + return { error: myError }; + } + + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/save', { + method: 'POST', + body: body, + headers: { + 'Content-Type': 'application/json', + }, + }) as any + ); + } + + /** + * Trash a Media on the server + * @param {Media} Media + * @return {*} + * @memberof UmbMediaDetailServerDataSource + */ + async trash(key: string) { + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/trash', { + method: 'POST', + body: JSON.stringify([key]), + headers: { + 'Content-Type': 'application/json', + }, + }) as any + ); + } + + /** + * Deletes a Template on the server + * @param {string} key + * @return {*} + * @memberof UmbTemplateDetailServerDataSource + */ + // TODO: Error mistake in this: + async delete(key: string) { + if (!key) { + const error: ProblemDetails = { title: 'Key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/delete', { + method: 'POST', + body: JSON.stringify([key]), + headers: { + 'Content-Type': 'application/json', + }, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts new file mode 100644 index 0000000000..47f5e59cbd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts @@ -0,0 +1,101 @@ +import type { RepositoryTreeDataSource } from '../../../../../../libs/repository/repository-tree-data-source.interface'; +import { ProblemDetails, MediaResource } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Media tree that fetches data from the server + * @export + * @class MediaTreeServerDataSource + * @implements {MediaTreeDataSource} + */ +export class MediaTreeServerDataSource implements RepositoryTreeDataSource { + #host: UmbControllerHostInterface; + + // TODO: how do we handle trashed items? + async trashItems(keys: Array) { + // TODO: use backend cli when available. + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/trash', { + method: 'POST', + body: JSON.stringify(keys), + headers: { + 'Content-Type': 'application/json', + }, + }) + ); + } + + async moveItems(keys: Array, destination: string) { + // TODO: use backend cli when available. + return tryExecuteAndNotify( + this.#host, + fetch('/umbraco/management/api/v1/media/move', { + method: 'POST', + body: JSON.stringify({ keys, destination }), + headers: { + 'Content-Type': 'application/json', + }, + }) + ); + } + + /** + * Creates an instance of MediaTreeServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof MediaTreeServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof MediaTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, MediaResource.getTreeMediaRoot({})); + } + + /** + * Fetches the children of a given parent key from the server + * @param {(string | null)} parentKey + * @return {*} + * @memberof MediaTreeServerDataSource + */ + async getChildrenOf(parentKey: string | null) { + if (!parentKey) { + const error: ProblemDetails = { title: 'Parent key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + MediaResource.getTreeMediaChildren({ + parentKey, + }) + ); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof MediaTreeServerDataSource + */ + async getItems(keys: Array) { + if (keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + MediaResource.getTreeMediaItem({ + key: keys, + }) + ); + } +} 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 9ab486dfda..53fccb3e97 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 { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from '../media.tree.store'; +import { UmbMediaRepository } from '../repository/media.repository'; 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: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), + repository: UmbMediaRepository, // TODO: use alias instead of class }, }; 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 33615dd9ae..34a4e9137d 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 @@ -1,4 +1,3 @@ -import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from '../media.tree.store'; import type { ManifestWorkspace, ManifestWorkspaceAction, @@ -59,7 +58,7 @@ const workspaceViewCollections: Array = [ pathname: 'collection', icon: 'umb:grid', entityType: 'media', - storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), + repositoryAlias: 'Umb.Repository.Media', }, }, ]; 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 d4f4cd51e5..ca13c9b3b1 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,31 +1,87 @@ -import { UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from "../media.detail.store"; -import { UmbEntityWorkspaceManager } from "../../../shared/components/workspace/workspace-context/entity-manager-controller"; -import { UmbWorkspaceContext } from "../../../shared/components/workspace/workspace-context/workspace-context"; -import { UmbWorkspaceEntityContextInterface } from "../../../shared/components/workspace/workspace-context/workspace-entity-context.interface"; -import type { MediaDetails } from "@umbraco-cms/models"; +import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; +import { UmbMediaRepository } from '../repository/media.repository'; +import type { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; +import { appendToFrozenArray, ObjectState } from '@umbraco-cms/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { MediaDetails } from '@umbraco-cms/models'; -export class UmbWorkspaceMediaContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { +type EntityType = MediaDetails; +export class UmbMediaWorkspaceContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ + #isNew = false; + #host: UmbControllerHostInterface; + #detailRepository: UmbMediaRepository; + #data = new ObjectState(undefined); + data = this.#data.asObservable(); + name = this.#data.getObservablePart((data) => data?.name); - #manager = new UmbEntityWorkspaceManager(this._host, 'media', UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN); + constructor(host: UmbControllerHostInterface) { + super(host); + this.#host = host; + this.#detailRepository = new UmbMediaRepository(this.#host); + } - public readonly data = this.#manager.state.asObservable(); - public readonly name = this.#manager.state.getObservablePart((state) => state?.name); + getData() { + return this.#data.getValue(); + } + + getEntityKey() { + return this.getData()?.key || ''; + } + + getEntityType() { + return 'media'; + } setName(name: string) { - this.#manager.state.update({name: name}) + this.#data.update({ name }); } - getEntityType = this.#manager.getEntityType; - getUnique = this.#manager.getEntityKey; - getEntityKey = this.#manager.getEntityKey; - getStore = this.#manager.getStore; - getData = this.#manager.getData; - load = this.#manager.load; - create = this.#manager.create; - save = this.#manager.save; - destroy = this.#manager.destroy; - public setPropertyValue(alias: string, value: unknown) { - throw new Error('setPropertyValue is not implemented for UmbWorkspaceMediaContext'); + setPropertyValue(alias: string, value: unknown) { + const entry = { alias: alias, value: value }; + + const currentData = this.#data.value; + if (currentData) { + const newDataSet = appendToFrozenArray(currentData.data, entry, (x) => x.alias); + + this.#data.update({ data: newDataSet }); + } + } + + async load(entityKey: string) { + const { data } = await this.#detailRepository.requestDetails(entityKey); + if (data) { + this.#isNew = false; + this.#data.next(data); + } + } + + async createScaffold(parentKey: string | null) { + const { data } = await this.#detailRepository.createDetailsScaffold(parentKey); + if (!data) return; + this.#isNew = true; + this.#data.next(data); + } + + async save() { + if (!this.#data.value) return; + if (this.#isNew) { + await this.#detailRepository.createDetail(this.#data.value); + } else { + await this.#detailRepository.saveDetail(this.#data.value); + } + // If it went well, then its not new anymore?. + this.#isNew = false; + } + + async delete(key: string) { + await this.#detailRepository.delete(key); + } + + public destroy(): void { + this.#data.complete(); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.element.ts index bb8be18f9f..c37bb50f6a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.element.ts @@ -1,11 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import { UmbWorkspaceMediaContext } from './media-workspace.context'; +import { css, html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface'; +import { UmbMediaWorkspaceContext } from './media-workspace.context'; import { UmbLitElement } from '@umbraco-cms/element'; -@customElement('umb-media-workspace') -export class UmbMediaWorkspaceElement extends UmbLitElement { +@customElement('umb-document-workspace') +export class UmbMediaWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { static styles = [ UUITextStyles, css` @@ -17,19 +18,31 @@ export class UmbMediaWorkspaceElement extends UmbLitElement { `, ]; + private _workspaceContext: UmbMediaWorkspaceContext = new UmbMediaWorkspaceContext(this); - private _workspaceContext: UmbWorkspaceMediaContext = new UmbWorkspaceMediaContext(this); + @state() + _unique?: string; - public load(value: string) { - this._workspaceContext?.load(value); + public load(entityKey: string) { + this._workspaceContext.load(entityKey); + this._unique = entityKey; } public create(parentKey: string | null) { - this._workspaceContext?.create(parentKey); + this._workspaceContext.createScaffold(parentKey); } render() { - return html``; + return html` + ${this._unique + ? html` + + ` + : nothing} + `; } } 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 3b5f60f050..5f7b6e8d29 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,3 @@ -import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media/media.tree.store'; import type { ManifestDashboardCollection, ManifestSection } from '@umbraco-cms/models'; const sectionAlias = 'Umb.Section.Media'; @@ -25,7 +24,7 @@ const dashboards: Array = [ sections: [sectionAlias], pathname: 'media-management', entityType: 'media', - storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), + repositoryAlias: 'Umb.Repository.Media', }, }, ]; 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 4dd097885f..c3073069e0 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 @@ -156,16 +156,6 @@ export class UmbCollectionContext< this.#selection.next(value); } - // TODO: Not all can trash, so maybe we need to differentiate on collection contexts or fix it with another architecture. - public trash(keys: string[]) { - this._store?.trash(keys); - } - - // TODO: Not all can move, so maybe we need to differentiate on collection contexts or fix it with another architecture. - public move(keys: string[], destination: string) { - this._store?.move(keys, destination); - } - public clearSelection() { this.#selection.next([]); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts index 81e529e706..313aea4af4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.element.ts @@ -5,7 +5,7 @@ import { map } from 'rxjs'; import './collection-selection-actions.element'; import './collection-toolbar.element'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from './collection.context'; -import { createExtensionElement , umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import type { ManifestCollectionView, MediaDetails } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; @@ -38,7 +38,7 @@ export class UmbCollectionElement extends UmbLitElement { private _collectionContext?: UmbCollectionContext; private _entityType!: string; - @property() + @property({ type: String, attribute: 'entity-type' }) public get entityType(): string { return this._entityType; } 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 54ea434ed3..d123be1282 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,7 +3,6 @@ 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 { UmbMediaTreeStore } from '../../../media/media/media.tree.store'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from '../../../shared/collection/collection.context'; import type { ManifestDashboardCollection } from '@umbraco-cms/models'; import type { FolderTreeItem } from '@umbraco-cms/backend-api'; @@ -25,7 +24,7 @@ export class UmbDashboardCollectionElement extends UmbLitElement { ]; // TODO: Use the right type here: - private _collectionContext?: UmbCollectionContext; + private _collectionContext?: UmbCollectionContext; public manifest!: ManifestDashboardCollection; @@ -37,14 +36,15 @@ export class UmbDashboardCollectionElement extends UmbLitElement { if (!this._collectionContext) { const manifestMeta = this.manifest.meta; - this._entityType = manifestMeta.entityType as string; - this._collectionContext = new UmbCollectionContext(this, null, null, manifestMeta.storeAlias); + const repositoryAlias = manifestMeta.repositoryAlias; + this._entityType = manifestMeta.entityType; + this._collectionContext = new UmbCollectionContext(this, this._entityType, null, '', repositoryAlias); this.provideContext(UMB_COLLECTION_CONTEXT_TOKEN, this._collectionContext); } } render() { - return html``; + return html``; } }