From 10c5d38a860a7f80ed87c8b6b6452a39682eeaf8 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Wed, 15 Feb 2023 19:06:02 +1000 Subject: [PATCH] repositories for media-types (#520) * repositories for media-types * consistent naming for repo alias * scaffold entity actions for media type * native private --------- Co-authored-by: Mads Rasmussen --- .../src/backoffice/backoffice.element.ts | 4 +- .../entity-actions/create.action.ts | 14 ++ .../media-types/entity-actions/manifests.ts | 80 ++++++++ .../entity-actions/reload.action.ts | 16 ++ .../backoffice/media/media-types/manifests.ts | 10 +- .../media-types/media-type.detail.store.ts | 60 ------ .../media-types/media-type.tree.store.ts | 67 ------- .../media/media-types/repository/manifests.ts | 13 ++ .../repository/media-type.detail.store.ts | 33 ++++ .../repository/media-type.repository.ts | 185 ++++++++++++++++++ .../repository/media-type.tree.store.ts | 25 +++ .../sources/media-type.detail.server.data.ts | 115 +++++++++++ ...edia-type.details.server.data.interface.ts | 10 + .../sources/media-type.tree.server.data.ts | 72 +++++++ .../media/media-types/tree/manifests.ts | 4 +- .../workspace/media-type-workspace.context.ts | 67 +++++++ .../workspace/media-type-workspace.element.ts | 62 +++++- .../workspace/data-type-workspace.element.ts | 29 ++- .../repository/dictionary.detail.store.ts | 2 +- .../repository/dictionary.repository.ts | 2 +- .../repository/dictionary.tree.store.ts | 2 +- 21 files changed, 713 insertions(+), 159 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/create.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/reload.action.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.detail.store.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.detail.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.details.server.data.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index fe7030042d..120db8ddc7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -17,8 +17,8 @@ import { } from './shared/components/backoffice-frame/backoffice.context'; import { UmbDocumentTypeStore } from './documents/document-types/repository/document-type.store'; import { UmbDocumentTypeTreeStore } from './documents/document-types/repository/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 { UmbMediaTypeDetailStore } from './media/media-types/repository/media-type.detail.store'; +import { UmbMediaTypeTreeStore } from './media/media-types/repository/media-type.tree.store'; import { UmbDocumentStore } from './documents/documents/repository/document.store'; import { UmbDocumentTreeStore } from './documents/documents/repository/document.tree.store'; import { UmbMediaDetailStore } from './media/media/repository/media.detail.store'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/create.action.ts new file mode 100644 index 0000000000..a2e0894606 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/create.action.ts @@ -0,0 +1,14 @@ +import { UmbMediaTypeRepository } from '../repository/media-type.repository'; +import { UmbEntityActionBase } from '../../../shared/entity-actions'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +export class UmbCreateMediaTypeEntityAction extends UmbEntityActionBase { + constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + console.log(`execute for: ${this.unique}`); + alert('open create dialog'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/manifests.ts new file mode 100644 index 0000000000..32332fbc1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/manifests.ts @@ -0,0 +1,80 @@ +import { UmbDeleteEntityAction } from '../../../../backoffice/shared/entity-actions/delete/delete.action'; +import { UmbMoveEntityAction } from '../../../../backoffice/shared/entity-actions/move/move.action'; +import { MEDIA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import { UmbCopyEntityAction } from '../../../../backoffice/shared/entity-actions/copy/copy.action'; +import { UmbCreateMediaTypeEntityAction } from './create.action'; +import UmbReloadMediaTypeEntityAction from './reload.action'; +import type { ManifestEntityAction } from '@umbraco-cms/models'; + +const entityType = 'media-type'; +const repositoryAlias = MEDIA_TYPE_REPOSITORY_ALIAS; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.MediaType.Create', + name: 'Create Media Type Entity Action', + weight: 500, + meta: { + entityType, + icon: 'umb:add', + label: 'Create', + repositoryAlias, + api: UmbCreateMediaTypeEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.MediaType.Move', + name: 'Move Media Type Entity Action', + weight: 400, + meta: { + entityType, + icon: 'umb:enter', + label: 'Move', + repositoryAlias, + api: UmbMoveEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.MediaType.Copy', + name: 'Copy Media Type Entity Action', + weight: 300, + meta: { + entityType, + icon: 'umb:documents', + label: 'Copy', + repositoryAlias, + api: UmbCopyEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.MediaType.Delete', + name: 'Delete Media Type Entity Action', + weight: 200, + meta: { + entityType, + icon: 'umb:trash', + label: 'Delete', + repositoryAlias, + api: UmbDeleteEntityAction, + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.MediaType.Reload', + name: 'Reload Media Type Entity Action', + weight: 100, + meta: { + entityType, + icon: 'umb:refresh', + label: 'Reload', + repositoryAlias, + api: UmbReloadMediaTypeEntityAction, + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/reload.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/reload.action.ts new file mode 100644 index 0000000000..f07f5b8767 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/entity-actions/reload.action.ts @@ -0,0 +1,16 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { UmbEntityActionBase } from '../../../shared/entity-actions'; +import { UmbMediaTypeRepository } from '../repository/media-type.repository'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +export default class UmbReloadMediaTypeEntityAction extends UmbEntityActionBase { + static styles = [UUITextStyles]; + + constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + alert('refresh') + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/manifests.ts index a4edc8b4f1..860a44f996 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/manifests.ts @@ -1,5 +1,13 @@ 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 repositoryManifests } from './repository/manifests'; +import { manifests as entityActionManifests } from './entity-actions/manifests'; -export const manifests = [...sidebarMenuItemManifests, ...treeManifests, ...workspaceManifests]; +export const manifests = [ + ...sidebarMenuItemManifests, + ...treeManifests, + ...repositoryManifests, + ...workspaceManifests, + ...entityActionManifests, +]; 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 deleted file mode 100644 index bb952da590..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.detail.store.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { MediaTypeDetails } from '@umbraco-cms/models'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbEntityDetailStore, 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 implements UmbEntityDetailStore { - private _data = new ArrayState([], (x) => x.key); - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); - } - - getScaffold(entityType: string, parentKey: string | null) { - return {} as MediaTypeDetails; - } - - /** - * @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 Media Type. - * @param {Array} mediaTypes - * @memberof UmbMediaTypesStore - * @return {*} {Promise} - */ - save(data: MediaTypeDetails[]) { - 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.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts deleted file mode 100644 index ff116139c1..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/media-type.tree.store.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { FolderTreeItemModel, MediaTypeResource } 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 } from '@umbraco-cms/store'; -import type { 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 ArrayState([], (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 this.#data.getObservablePart((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 this.#data.getObservablePart((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 this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts new file mode 100644 index 0000000000..2460e11e38 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbMediaTypeRepository } from './media-type.repository'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; + +export const MEDIA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.MediaTypes'; + +const repository: ManifestRepository = { + type: 'repository', + alias: MEDIA_TYPE_REPOSITORY_ALIAS, + name: 'Media Types Repository', + class: UmbMediaTypeRepository, +}; + +export const manifests = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts new file mode 100644 index 0000000000..c6dfb6cbd7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts @@ -0,0 +1,33 @@ +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import type { MediaTypeDetails } from '@umbraco-cms/models'; + +/** + * @export + * @class UmbMediaTypeDetailStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Media Types + */ +export class UmbMediaTypeDetailStore + extends UmbStoreBase +{ + #data = new ArrayState([], (x) => x.key); + + constructor(host: UmbControllerHostInterface) { + super(host, UmbMediaTypeDetailStore.name); + } + + append(mediaType: MediaTypeDetails) { + this.#data.append([mediaType]); + } + + remove(uniques: string[]) { + this.#data.remove(uniques); + } +} + +export const UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbMediaTypeDetailStore.name +); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts new file mode 100644 index 0000000000..71c8a21df6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts @@ -0,0 +1,185 @@ +import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from "./media-type.tree.store"; +import { UmbMediaTypeDetailServerDataSource } from "./sources/media-type.detail.server.data"; +import { UmbMediaTypeDetailStore, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from "./media-type.detail.store"; +import { MediaTypeTreeServerDataSource } from "./sources/media-type.tree.server.data"; +import { ProblemDetailsModel } from "@umbraco-cms/backend-api"; +import { UmbContextConsumerController } from "@umbraco-cms/context-api"; +import { UmbControllerHostInterface } from "@umbraco-cms/controller"; +import type { MediaTypeDetails } from "@umbraco-cms/models"; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from "@umbraco-cms/notification"; +import { UmbTreeRepository, RepositoryTreeDataSource } from "@umbraco-cms/repository"; + +export class UmbMediaTypeRepository implements UmbTreeRepository { + #init!: Promise; + + #host: UmbControllerHostInterface; + + #treeSource: RepositoryTreeDataSource; + #treeStore?: UmbMediaTypeTreeStore; + + #detailSource: UmbMediaTypeDetailServerDataSource; + #detailStore?: UmbMediaTypeDetailStore; + + #notificationService?: UmbNotificationService; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + // TODO: figure out how spin up get the correct data source + this.#treeSource = new MediaTypeTreeServerDataSource(this.#host); + this.#detailSource = new UmbMediaTypeDetailServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { + this.#detailStore = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = 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: ProblemDetailsModel = { 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: ProblemDetailsModel = { 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() { + await this.#init; + return this.#detailSource.createScaffold(); + } + + 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: ProblemDetailsModel = { title: 'Key is missing' }; + return { error }; + } + const { data, error } = await this.#detailSource.get(key); + + if (data) { + this.#detailStore?.append(data); + } + return { data, error }; + } + + async delete(key: string) { + await this.#init; + return this.#detailSource.delete(key); + } + + async saveDetail(mediaType: MediaTypeDetails) { + await this.#init; + + // TODO: should we show a notification if the media type is missing? + // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? + if (!mediaType || !mediaType.key) { + const error: ProblemDetailsModel = { title: 'Media Type is missing' }; + return { error }; + } + + const { error } = await this.#detailSource.update(mediaType); + + if (!error) { + const notification = { data: { message: `Media type '${mediaType.name}' 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 media type is updated in the store while someone is editing it. + this.#detailStore?.append(mediaType); + this.#treeStore?.updateItem(mediaType.key, { name: mediaType.name }); + // TODO: would be nice to align the stores on methods/methodNames. + + return { error }; + } + + async createDetail(mediaType: MediaTypeDetails) { + await this.#init; + + if (!mediaType.name) { + const error: ProblemDetailsModel = { title: 'Name is missing' }; + return { error }; + } + + const { data, error } = await this.#detailSource.insert(mediaType); + + if (!error) { + const notification = { data: { message: `Media type '${mediaType.name}' created` } }; + this.#notificationService?.peek('positive', notification); + } + + return { data, error }; + } + + async move() { + alert('move me!'); + } + + async copy() { + alert('copy me'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts new file mode 100644 index 0000000000..fa1d2ab1dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts @@ -0,0 +1,25 @@ +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbTreeStoreBase } from '@umbraco-cms/store'; +import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/** + * @export + * @class UmbMediaTypeTreeStore + * @extends {UmbTreeStoreBase} + * @description - Tree Data Store for Media Types + */ +export class UmbMediaTypeTreeStore extends UmbTreeStoreBase { + + /** + * Creates an instance of UmbMediaTypeTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbMediaTypeTreeStore + */ + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); + } +} + +export const UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UmbMediaTypeTreeStore.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.detail.server.data.ts new file mode 100644 index 0000000000..d0fe79bfc7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.detail.server.data.ts @@ -0,0 +1,115 @@ +import { MediaTypeDetailDataSource } from './media-type.details.server.data.interface'; +import { ProblemDetailsModel } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { MediaTypeDetails } from '@umbraco-cms/models'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * @description - A data source for the Media Type detail that fetches data from the server + * @export + * @class UmbMediaTypeDetailServerDataSource + * @implements {MediaTypeDetailDataSource} + */ +export class UmbMediaTypeDetailServerDataSource implements MediaTypeDetailDataSource { + #host: UmbControllerHostInterface; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * @description - Creates a new MediaType scaffold + * @return {*} + * @memberof UmbMediaTypeDetailServerDataSource + */ + async createScaffold() { + const data: MediaTypeDetails = { + name: '', + } as MediaTypeDetails; + + return { data }; + } + + /** + * @description - Fetches a MediaType with the given key from the server + * @param {string} key + * @return {*} + * @memberof UmbMediaTypeDetailServerDataSource + */ + get(key: string) { + //return tryExecuteAndNotify(this.#host, MediaTypeResource.getMediaTypeByKey({ key })) as any; + // TODO: use backend cli when available. + return tryExecuteAndNotify(this.#host, fetch(`/umbraco/management/api/v1/media-type/${key}`)) as any; + } + + /** + * @description - Updates a MediaType on the server + * @param {MediaTypeDetails} MediaType + * @return {*} + * @memberof UmbMediaTypeDetailServerDataSource + */ + async update(mediaType: MediaTypeDetails) { + if (!mediaType.key) { + const error: ProblemDetailsModel = { title: 'MediaType key is missing' }; + return { error }; + } + + const payload = { key: mediaType.key, requestBody: mediaType }; + //return tryExecuteAndNotify(this.#host, MediaTypeResource.putMediaTypeByKey(payload)); + + // TODO: use backend cli when available. + return tryExecuteAndNotify( + this.#host, + fetch(`/umbraco/management/api/v1/media-type/${mediaType.key}`, { + method: 'PUT', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }) + ) as any; + } + + /** + * @description - Inserts a new MediaType on the server + * @param {MediaTypeDetails} data + * @return {*} + * @memberof UmbMediaTypeDetailServerDataSource + */ + async insert(data: MediaTypeDetails) { + //return tryExecuteAndNotify(this.#host, MediaTypeResource.postMediaType({ requestBody: data })); + // TODO: use backend cli when available. + return tryExecuteAndNotify( + this.#host, + fetch(`/umbraco/management/api/v1/media-type/`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + }) + ) as any; + } + + /** + * @description - Deletes a MediaType on the server + * @param {string} key + * @return {*} + * @memberof UmbMediaTypeDetailServerDataSource + */ + async delete(key: string) { + if (!key) { + const error: ProblemDetailsModel = { title: 'Key is missing' }; + return { error }; + } + + //return await tryExecuteAndNotify(this.#host, MediaTypeResource.deleteMediaTypeByKey({ key })); + // TODO: use backend cli when available. + return tryExecuteAndNotify( + this.#host, + fetch(`/umbraco/management/api/v1/media-type/${key}`, { + method: 'DELETE', + }) + ) as any; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.details.server.data.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.details.server.data.interface.ts new file mode 100644 index 0000000000..b3a4306511 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.details.server.data.interface.ts @@ -0,0 +1,10 @@ +import type { DataSourceResponse, MediaTypeDetails } from '@umbraco-cms/models'; + +// TODO => Use models when they exist +export interface MediaTypeDetailDataSource { + createScaffold(parentKey: string): Promise>; + get(key: string): Promise>; + insert(data: any): Promise; + update(data: any): Promise; + delete(key: string): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts new file mode 100644 index 0000000000..b24810721b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts @@ -0,0 +1,72 @@ +import { MediaTypeResource, ProblemDetailsModel } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { RepositoryTreeDataSource } from '@umbraco-cms/repository'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the MediaType tree that fetches data from the server + * @export + * @class MediaTypeTreeServerDataSource + * @implements {MediaTypeTreeDataSource} + */ +export class MediaTypeTreeServerDataSource implements RepositoryTreeDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of MediaTypeTreeDataSource. + * @param {UmbControllerHostInterface} host + * @memberof MediaTypeTreeDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches the root items for the tree from the server + * @return {*} + * @memberof MediaTypeTreeServerDataSource + */ + async getRootItems() { + return tryExecuteAndNotify(this.#host, MediaTypeResource.getTreeMediaTypeRoot({})); + } + + /** + * Fetches the children of a given parent key from the server + * @param {(string | null)} parentKey + * @return {*} + * @memberof MediaTypeTreeServerDataSource + */ + async getChildrenOf(parentKey: string | null) { + if (!parentKey) { + const error: ProblemDetailsModel = { title: 'Parent key is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + MediaTypeResource.getTreeMediaTypeChildren({ + parentKey, + }) + ); + } + + /** + * Fetches the items for the given keys from the server + * @param {Array} keys + * @return {*} + * @memberof MediaTypeTreeServerDataSource + */ + async getItems(keys: Array) { + if (!keys || keys.length === 0) { + const error: ProblemDetailsModel = { title: 'Keys are missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + MediaTypeResource.getTreeMediaTypeItem({ + key: keys, + }) + ); + } +} 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 d83d3c5967..de40b07cde 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 { UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from '../media-type.tree.store'; +import { UmbMediaTypeRepository } from '../repository/media-type.repository'; 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: UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString(), + repository: UmbMediaTypeRepository }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts new file mode 100644 index 0000000000..4433b62d75 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts @@ -0,0 +1,67 @@ +import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; +import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; +import { UmbMediaTypeRepository } from '../repository/media-type.repository'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { ObjectState } from '@umbraco-cms/observable-api'; +import type { MediaTypeDetails } from '@umbraco-cms/models'; + +type EntityType = MediaTypeDetails; +export class UmbWorkspaceMediaTypeContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ + #host: UmbControllerHostInterface; + #repo: UmbMediaTypeRepository; + + #data = new ObjectState(undefined); + data = this.#data.asObservable(); + name = this.#data.getObservablePart((data) => data?.name); + + constructor(host: UmbControllerHostInterface) { + super(host); + this.#host = host; + this.#repo = new UmbMediaTypeRepository(this.#host); + } + + getData() { + return this.#data.getValue(); + } + + getEntityKey() { + return this.getData()?.key || ''; + } + + getEntityType() { + return 'media-type'; + } + + setName(name: string) { + this.#data.update({ name }); + } + + setPropertyValue(alias: string, value: string) { + // TODO => Implement setPropertyValue + } + + async load(entityKey: string) { + const { data } = await this.#repo.requestDetails(entityKey); + if (data) { + this.#data.next(data); + } + } + + async createScaffold() { + const { data } = await this.#repo.createDetailsScaffold(); + if (!data) return; + this.#data.next(data); + } + + async save() { + if (!this.#data.value) return; + this.#repo.saveDetail(this.#data.value); + } + + public destroy(): void { + this.#data.complete(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.element.ts index e605df5e74..10c235c581 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.element.ts @@ -1,27 +1,71 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html, LitElement } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -import '../../../shared/components/workspace/workspace-layout/workspace-layout.element'; +import { css, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; +import { UmbWorkspaceEntityElement } from '../../../../backoffice/shared/components/workspace/workspace-entity-element.interface'; +import { UmbWorkspaceMediaTypeContext } from './media-type-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-media-type-workspace') -export class UmbMediaTypeWorkspaceElement extends LitElement { +export class UmbMediaTypeWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { static styles = [ UUITextStyles, css` - :host { - display: block; + #header { + display: flex; + padding: 0 var(--uui-size-space-6); + gap: var(--uui-size-space-4); + width: 100%; + } + uui-input { width: 100%; - height: 100%; } `, ]; + @state() + private _unique?: string; + + @state() + private _mediaTypeName?: string | null = ''; + @property() id!: string; + #workspaceContext = new UmbWorkspaceMediaTypeContext(this); + + public load(entityKey: string) { + this.#workspaceContext.load(entityKey); + this._unique = entityKey; + } + + public create() { + this.#workspaceContext.createScaffold(); + } + + async connectedCallback() { + super.connectedCallback(); + + this.observe(this.#workspaceContext.name, (name) => { + this._mediaTypeName = name; + }); + } + + // TODO. find a way where we don't have to do this for all Workspaces. + #handleInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext.setName(target.value); + } + } + } + render() { - return html`Media Type Workspace`; + return html` + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.element.ts index e49050eb53..f6bd4c35b6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.element.ts @@ -2,7 +2,6 @@ import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { distinctUntilChanged } from 'rxjs'; import { UmbWorkspaceDataTypeContext } from './data-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -29,23 +28,23 @@ export class UmbDataTypeWorkspaceElement extends UmbLitElement { `, ]; - private _workspaceContext: UmbWorkspaceDataTypeContext = new UmbWorkspaceDataTypeContext(this); - - public load(value: string) { - this._workspaceContext?.load(value); - //this._unique = entityKey; - } - - public create(parentKey: string | null) { - this._workspaceContext.createScaffold(parentKey); - } + #workspaceContext: UmbWorkspaceDataTypeContext = new UmbWorkspaceDataTypeContext(this); @state() private _dataTypeName = ''; + public load(value: string) { + this.#workspaceContext?.load(value); + //this._unique = entityKey; + } + + public create(parentKey: string | null) { + this.#workspaceContext.createScaffold(parentKey); + } + constructor() { super(); - this.observe(this._workspaceContext.name, (dataTypeName) => { + this.observe(this.#workspaceContext.name, (dataTypeName) => { if (dataTypeName !== this._dataTypeName) { this._dataTypeName = dataTypeName ?? ''; } @@ -53,12 +52,12 @@ export class UmbDataTypeWorkspaceElement extends UmbLitElement { } // TODO. find a way where we don't have to do this for all Workspaces. - private _handleInput(event: UUIInputEvent) { + #handleInput(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { - this._workspaceContext.setName(target.value); + this.#workspaceContext.setName(target.value); } } } @@ -66,7 +65,7 @@ export class UmbDataTypeWorkspaceElement extends UmbLitElement { render() { return html` - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts index 4b1ab81c72..9139f184b4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts @@ -8,7 +8,7 @@ import type { DictionaryDetails } from '@umbraco-cms/models'; * @export * @class UmbDictionaryDetailStore * @extends {UmbStoreBase} - * @description - Details Data Store for Data Types + * @description - Details Data Store for Dictionary */ export class UmbDictionaryDetailStore extends UmbStoreBase diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts index 6315120503..acbf89e38f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts @@ -1,7 +1,7 @@ import { DictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data'; import { UmbDictionaryTreeStore, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from './dictionary.tree.store'; -import { UmbDictionaryDetailStore, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN } from './dictionary.detail.store'; import { UmbDictionaryDetailServerDataSource } from './sources/dictionary.detail.server.data'; +import { UmbDictionaryDetailStore, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN } from './dictionary.detail.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; import { RepositoryTreeDataSource, UmbTreeRepository } from '@umbraco-cms/repository'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts index e0df7e7b23..d71ec0afcf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts @@ -6,7 +6,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/controller'; * @export * @class UmbDictionaryTreeStore * @extends {UmbTreeStoreBase} - * @description - Tree Data Store for Data Types + * @description - Tree Data Store for Dictionary */ export class UmbDictionaryTreeStore extends UmbTreeStoreBase {