From 424beea16e83531ed59b196f1a489e43dedfa998 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 31 Mar 2023 13:41:14 +0200 Subject: [PATCH] Feature: Create data type folders (1. round) (#630) * update data type handlers * call correct method on repo * introduce base menu item element * wip register create and delete entity actions for data types * stop event * add folder request interceptor * remove todo * create interface for folder data source * create interface for folder repository * open create dialog in data type create action * wip create folder modal * add token * clean up data type repo * add has children prop * add create folder modal * add folder to temp data * update import * implement delete folder action * add method to update folder * update detail data source interface to follow backend models * lint fixes * move import fixes * make generic folder modal * add correct request model * add put interceptor * fix import * update data type notification headline --- .../delete-folder/delete-folder.action.ts | 39 ++++ .../actions/delete/delete.action.ts | 4 +- .../folder-update/folder-update.action.ts | 30 +++ .../libs/entity-action/actions/index.ts | 2 + .../libs/entity-action/entity-action.ts | 2 + .../libs/modal/token/folder-modal.token.ts | 16 ++ .../libs/modal/token/index.ts | 1 + .../data-source/data-source.interface.ts | 12 +- .../folder-data-source.interface.ts | 14 ++ .../libs/repository/data-source/index.ts | 1 + .../repository/folder-repository.interface.ts | 35 ++++ .../libs/repository/index.ts | 1 + .../repository/document-type.repository.ts | 2 +- .../sources/document-type.server.data.ts | 38 +--- .../repository/document.repository.ts | 10 +- .../sources/document.server.data.ts | 19 +- .../documents/repository/sources/index.ts | 2 +- .../workspace/document-workspace.context.ts | 6 +- .../media/repository/media.repository.ts | 2 +- .../sources/media.detail.server.data.ts | 4 +- .../repository/member-group.repository.ts | 2 +- .../member-group.detail.server.data.ts | 4 +- .../entity-actions/create/create.action.ts | 25 +++ .../entity-actions/create/manifests.ts | 29 +++ .../modal/create-data-type-modal.element.ts | 56 +++++ .../entity-actions/create/modal/index.ts | 6 + .../data-types/entity-actions/manifests.ts | 58 ++++++ .../settings/data-types/manifests.ts | 9 +- .../data-types/menu-item/manifests.ts | 2 +- .../repository/data-type.repository.ts | 192 ++++++++++++------ .../sources/data-type-folder.server.data.ts | 112 ++++++++++ .../sources/data-type.server.data.ts | 80 ++------ .../sources/data-type.tree.server.data.ts | 60 +----- .../repository/language.repository.ts | 4 +- .../languages/repository/sources/index.ts | 15 -- .../sources/language.server.data.ts | 33 ++- .../repository/relation-type.repository.ts | 2 +- .../sources/relation-type.server.data.ts | 70 ++----- .../src/backoffice/shared/components/index.ts | 2 + .../components/menu-item/menu-item.element.ts | 55 ----- .../menu-item-base/menu-item-base.element.ts | 125 ++++++++++++ .../menu/menu-item/menu-item.element.ts | 31 +++ .../shared/components/menu/menu.element.ts | 2 +- .../section-sidebar.context.ts | 7 +- .../tree-menu-item/tree-menu-item.element.ts | 20 +- .../modals/folder/folder-modal.element.ts | 177 ++++++++++++++++ .../src/backoffice/shared/modals/manifests.ts | 6 + .../sources/stylesheet.server.data.ts | 6 +- .../src/core/mocks/data/data-type.data.ts | 42 +++- .../core/mocks/domains/data-type.handlers.ts | 92 +++++++-- 50 files changed, 1138 insertions(+), 426 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/entity-action/actions/folder-update/folder-update.action.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/modal/token/folder-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/repository/data-source/folder-data-source.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/repository/folder-repository.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/create.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/create-data-type-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-folder.server.data.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu-item/menu-item.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item-base/menu-item-base.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item/menu-item.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/folder/folder-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts new file mode 100644 index 0000000000..9a2adadb0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts @@ -0,0 +1,39 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal'; + +export class UmbDeleteFolderEntityAction< + T extends { deleteFolder(unique: string): Promise; requestTreeItems(uniques: Array): any } +> extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + if (!this.repository || !this.#modalContext) return; + + const { data } = await this.repository.requestTreeItems([this.unique]); + + if (data) { + const item = data[0]; + + // TODO: maybe we can show something about how many items are part of the folder? + const modalHandler = this.#modalContext.open(UMB_CONFIRM_MODAL, { + headline: `Delete folder ${item.name}`, + content: 'Are you sure you want to delete this folder?', + color: 'danger', + confirmLabel: 'Delete', + }); + + await modalHandler.onSubmit(); + await this.repository?.deleteFolder(this.unique); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts index 0dfc13f22d..d290a1f912 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts @@ -4,7 +4,7 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal'; export class UmbDeleteEntityAction< - T extends { delete(unique: string): Promise; requestItems(uniques: Array): any } + T extends { delete(unique: string): Promise; requestTreeItems(uniques: Array): any } > extends UmbEntityActionBase { #modalContext?: UmbModalContext; @@ -19,7 +19,7 @@ export class UmbDeleteEntityAction< async execute() { if (!this.repository || !this.#modalContext) return; - const { data } = await this.repository.requestItems([this.unique]); + const { data } = await this.repository.requestTreeItems([this.unique]); if (data) { const item = data[0]; diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/folder-update/folder-update.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/folder-update/folder-update.action.ts new file mode 100644 index 0000000000..2a1fc97ece --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/folder-update/folder-update.action.ts @@ -0,0 +1,30 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_FOLDER_MODAL, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; +import { UmbFolderRepository } from '@umbraco-cms/backoffice/repository'; + +export class UmbFolderUpdateEntityAction< + T extends UmbFolderRepository = UmbFolderRepository +> extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + if (!this.repository || !this.#modalContext) return; + + const modalHandler = this.#modalContext.open(UMB_FOLDER_MODAL, { + repositoryAlias: this.repositoryAlias, + unique: this.unique, + }); + + await modalHandler.onSubmit(); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/index.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/index.ts index b8fa3d3206..8551f08139 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/index.ts @@ -1,5 +1,7 @@ export * from './copy/copy.action'; export * from './delete/delete.action'; +export * from './delete-folder/delete-folder.action'; +export * from './folder-update/folder-update.action'; export * from './move/move.action'; export * from './sort-children-of/sort-children-of.action'; export * from './trash/trash.action'; diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/entity-action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/entity-action.ts index c0235ae958..5c2d816deb 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/entity-action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/entity-action.ts @@ -7,9 +7,11 @@ export interface UmbEntityAction extends UmbAction extends UmbActionBase { unique: string; + repositoryAlias: string; constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { super(host, repositoryAlias); this.unique = unique; + this.repositoryAlias = repositoryAlias; } } diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/folder-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/folder-modal.token.ts new file mode 100644 index 0000000000..f842409751 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/folder-modal.token.ts @@ -0,0 +1,16 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { FolderReponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export interface UmbFolderModalData { + repositoryAlias: string; + unique?: string; +} + +export interface UmbFolderModalResult { + folder: FolderReponseModel; +} + +export const UMB_FOLDER_MODAL = new UmbModalToken('Umb.Modal.Folder', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts index ed363f7165..7fd02d418b 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts @@ -25,3 +25,4 @@ export * from './template-modal.token'; export * from './template-picker-modal.token'; export * from './user-group-picker-modal.token'; export * from './user-picker-modal.token'; +export * from './folder-modal.token'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts index 766ddf1e87..13cfbfe7a5 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source.interface.ts @@ -1,9 +1,9 @@ import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; -export interface UmbDataSource { - createScaffold(parentKey: string | null): Promise>; - get(key: string): Promise>; - insert(data: T): Promise>; - update(data: T): Promise>; - delete(key: string): Promise>; +export interface UmbDataSource { + createScaffold(parentKey: string | null): Promise>; + get(unique: string): Promise>; + insert(data: CreateRequestType): Promise; + update(unique: string, data: UpdateRequestType): Promise>; + delete(unique: string): Promise; } diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/folder-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/folder-data-source.interface.ts new file mode 100644 index 0000000000..c07bf208f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/folder-data-source.interface.ts @@ -0,0 +1,14 @@ +import { DataSourceResponse } from './data-source-response.interface'; +import { + CreateFolderRequestModel, + FolderReponseModel, + UpdateFolderReponseModel, +} from '@umbraco-cms/backoffice/backend-api'; + +export interface UmbFolderDataSource { + createScaffold(parentKey: string | null): Promise>; + get(unique: string): Promise>; + insert(data: CreateFolderRequestModel): Promise>; + update(unique: string, data: CreateFolderRequestModel): Promise>; + delete(unique: string): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts index 0c12e00d34..905a6849fe 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts @@ -1,3 +1,4 @@ export * from './data-source-response.interface'; export * from './data-source.interface'; +export * from './folder-data-source.interface'; export * from './tree-data-source.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/folder-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/folder-repository.interface.ts new file mode 100644 index 0000000000..4a021f2bd0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/folder-repository.interface.ts @@ -0,0 +1,35 @@ +import type { + CreateFolderRequestModel, + FolderModelBaseModel, + FolderReponseModel, + ProblemDetailsModel, + UpdateFolderReponseModel, +} from '@umbraco-cms/backoffice/backend-api'; + +export interface UmbFolderRepository { + createFolderScaffold(parentKey: string | null): Promise<{ + data?: FolderReponseModel; + error?: ProblemDetailsModel; + }>; + createFolder(folderRequest: CreateFolderRequestModel): Promise<{ + data?: string; + error?: ProblemDetailsModel; + }>; + + requestFolder(unique: string): Promise<{ + data?: FolderReponseModel; + error?: ProblemDetailsModel; + }>; + + updateFolder( + unique: string, + folder: FolderModelBaseModel + ): Promise<{ + data?: UpdateFolderReponseModel; + error?: ProblemDetailsModel; + }>; + + deleteFolder(key: string): Promise<{ + error?: ProblemDetailsModel; + }>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/index.ts index f4727c8ffc..9dcba782ba 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/index.ts @@ -1,3 +1,4 @@ export * from './data-source'; export * from './detail-repository.interface'; export * from './tree-repository.interface'; +export * from './folder-repository.interface'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts index 24eeb55d75..14494da134 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts @@ -164,7 +164,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U if (!item || !item.key) throw new Error('Document-Type is missing'); await this.#init; - const { error } = await this.#detailDataSource.update(item); + const { error } = await this.#detailDataSource.update(item.key, item); if (!error) { const notification = { data: { message: `Document saved` } }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts index 5f96641f64..561d27c049 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts @@ -13,7 +13,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDocumentTypeServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentTypeServerDataSource implements UmbDataSource { +export class UmbDocumentTypeServerDataSource implements UmbDataSource { #host: UmbControllerHostElement; /** @@ -82,7 +82,7 @@ export class UmbDocumentTypeServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, fetch('/umbraco/management/api/v1/document-type', { method: 'POST', @@ -100,13 +100,8 @@ export class UmbDocumentTypeServerDataSource implements UmbDataSource( - this.#host, - fetch(`/umbraco/management/api/v1/document-type/${key}`, { - method: 'DELETE', - body: JSON.stringify([key]), - headers: { - 'Content-Type': 'application/json', - }, - }) as any - ); - } - /** * Deletes a Template on the server * @param {string} key diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index f1b76e815c..46b2efdabd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -5,7 +5,11 @@ import { DocumentTreeServerDataSource } from './sources/document.tree.server.dat import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { ProblemDetailsModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + ProblemDetailsModel, + DocumentResponseModel, + CreateDocumentRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; type ItemType = DocumentResponseModel; @@ -133,7 +137,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDe // Could potentially be general methods: - async create(item: ItemType) { + async create(item: CreateDocumentRequestModel & { key: string }) { await this.#init; if (!item || !item.key) { @@ -162,7 +166,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDe throw new Error('Document is missing'); } - const { error } = await this.#detailDataSource.update(item); + const { error } = await this.#detailDataSource.update(item.key, item); if (!error) { const notification = { data: { message: `Document saved` } }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts index 20de768ab9..83a93a9c1a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts @@ -5,6 +5,8 @@ import { ProblemDetailsModel, DocumentResponseModel, ContentStateModel, + CreateDocumentRequestModel, + UpdateDocumentRequestModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -15,7 +17,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDocumentServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentServerDataSource implements UmbDataSource { +export class UmbDocumentServerDataSource + implements UmbDataSource +{ #host: UmbControllerHostElement; /** @@ -83,12 +87,8 @@ export class UmbDocumentServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, fetch('/umbraco/management/api/v1/document/save', { method: 'POST', @@ -118,8 +118,7 @@ export class UmbDocumentServerDataSource implements UmbDataSource { +export interface UmbDocumentDataSource extends UmbDataSource { createScaffold(documentTypeKey: string): Promise>; trash(key: string): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index 63c0f26405..246b1866a6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -5,7 +5,7 @@ import { UmbWorkspaceVariableEntityContextInterface } from '../../../shared/comp import { UmbVariantId } from '../../../shared/variants/variant-id.class'; import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-property-structure-manager.class'; import { UmbWorkspaceSplitViewManager } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; -import type { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import type { CreateDocumentRequestModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { partialUpdateFrozenArray, ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -164,7 +164,9 @@ export class UmbDocumentWorkspaceContext async save() { if (!this.#draft.value) return; if (this.getIsNew()) { - await this.repository.create(this.#draft.value); + // TODO: typescript hack until we get the create type + const value = this.#draft.value as CreateDocumentRequestModel & { key: string }; + await this.repository.create(value); } else { await this.repository.save(this.#draft.value); } 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 index c2de9efe12..873e8011d6 100644 --- 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 @@ -179,7 +179,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor throw new Error('Template is missing'); } - const { error } = await this.#detailDataSource.update(document); + const { error } = await this.#detailDataSource.update(document.key, document); if (!error) { const notification = { data: { message: `Document saved` } }; 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 index 416090e87e..609a119df6 100644 --- 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 @@ -10,7 +10,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbTemplateDetailServerDataSource * @implements {TemplateDetailDataSource} */ -export class UmbMediaDetailServerDataSource implements UmbDataSource { +export class UmbMediaDetailServerDataSource implements UmbDataSource { #host: UmbControllerHostElement; /** @@ -124,7 +124,7 @@ export class UmbMediaDetailServerDataSource implements UmbDataSource Provide type when it is available -export class UmbMemberGroupDetailServerDataSource implements UmbDataSource { +export class UmbMemberGroupDetailServerDataSource implements UmbDataSource { #host: UmbControllerHostElement; constructor(host: UmbControllerHostElement) { @@ -49,7 +49,7 @@ export class UmbMemberGroupDetailServerDataSource implements UmbDataSource * @return {*} * @memberof UmbMemberGroupDetailServerDataSource */ - async update(memberGroup: MemberGroupDetails) { + async update(key: string, memberGroup: MemberGroupDetails) { if (!memberGroup.key) { const error: ProblemDetailsModel = { title: 'Member Group key is missing' }; return { error }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/create.action.ts new file mode 100644 index 0000000000..5849726dd0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/create.action.ts @@ -0,0 +1,25 @@ +import { UmbDataTypeRepository } from '../../repository/data-type.repository'; +import { UMB_CREATE_DATA_TYPE_MODAL } from './modal'; +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; + +export class UmbCreateDataTypeEntityAction extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + // TODO: what to do if modal service is not available? + if (!this.#modalContext) return; + if (!this.repository) return; + this.#modalContext?.open(UMB_CREATE_DATA_TYPE_MODAL); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/manifests.ts new file mode 100644 index 0000000000..e564e04fc6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/manifests.ts @@ -0,0 +1,29 @@ +import { DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/manifests'; +import { UmbCreateDataTypeEntityAction } from './create.action'; +import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.Create', + name: 'Create Data Type Entity Action', + weight: 900, + meta: { + icon: 'umb:add', + label: 'Create', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbCreateDataTypeEntityAction, + }, + conditions: { + entityType: 'data-type-root', + }, + }, + { + type: 'modal', + alias: 'Umb.Modal.CreateDataType', + name: 'Create Data Type Modal', + loader: () => import('./modal/create-data-type-modal.element'), + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/create-data-type-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/create-data-type-modal.element.ts new file mode 100644 index 0000000000..cfe48669f7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/create-data-type-modal.element.ts @@ -0,0 +1,56 @@ +import { html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement } from 'lit/decorators.js'; +import { DATA_TYPE_REPOSITORY_ALIAS } from '../../../repository/manifests'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbModalContext, UMB_FOLDER_MODAL, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-create-data-type-modal') +export class UmbCreateDataTypeModalElement extends UmbModalBaseElement { + static styles = [UUITextStyles]; + + #modalContext?: UmbModalContext; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + #onClick(event: PointerEvent) { + event.stopPropagation(); + const folderModalHandler = this.#modalContext?.open(UMB_FOLDER_MODAL, { + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + }); + folderModalHandler?.onSubmit().then(() => this.modalHandler?.submit()); + } + + #onCancel() { + this.modalHandler?.reject(); + } + + render() { + return html` + + + + } + + + } + + + Cancel + + `; + } +} + +export default UmbCreateDataTypeModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-create-data-type-modal': UmbCreateDataTypeModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/index.ts new file mode 100644 index 0000000000..9e1e8eb025 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/create/modal/index.ts @@ -0,0 +1,6 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export const UMB_CREATE_DATA_TYPE_MODAL = new UmbModalToken('Umb.Modal.CreateDataType', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts new file mode 100644 index 0000000000..54e8288b16 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts @@ -0,0 +1,58 @@ +import { DATA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; +import { manifests as createManifests } from './create/manifests'; +import { + UmbDeleteEntityAction, + UmbDeleteFolderEntityAction, + UmbFolderUpdateEntityAction, +} from '@umbraco-cms/backoffice/entity-action'; +import { ManifestEntityAction } from '@umbraco-cms/backoffice/extensions-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.Delete', + name: 'Delete Data Type Entity Action', + weight: 900, + meta: { + icon: 'umb:trash', + label: 'Delete...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbDeleteEntityAction, + }, + conditions: { + entityType: 'data-type', + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.DeleteFolder', + name: 'Delete Data Type Folder Entity Action', + weight: 800, + meta: { + icon: 'umb:trash', + label: 'Delete Folder...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbDeleteFolderEntityAction, + }, + conditions: { + entityType: 'data-type', + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.RenameFolder', + name: 'Rename Data Type Folder Entity Action', + weight: 700, + meta: { + icon: 'umb:edit', + label: 'Rename Folder...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbFolderUpdateEntityAction, + }, + conditions: { + entityType: 'data-type', + }, + }, +]; + +export const manifests = [...entityActions, ...createManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts index ddaa4babdc..fac58d2628 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts @@ -1,6 +1,13 @@ +import { manifests as entityActions } from './entity-actions/manifests'; import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as menuItemManifests } from './menu-item/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; -export const manifests = [...repositoryManifests, ...menuItemManifests, ...treeManifests, ...workspaceManifests]; +export const manifests = [ + ...entityActions, + ...repositoryManifests, + ...menuItemManifests, + ...treeManifests, + ...workspaceManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/menu-item/manifests.ts index 29c995c92a..1f44f2d881 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/menu-item/manifests.ts @@ -9,7 +9,7 @@ const menuItem: ManifestTypes = { meta: { label: 'Data Types', icon: 'umb:folder', - entityType: 'data-type', + entityType: 'data-type-root', treeAlias: 'Umb.Tree.DataTypes', }, conditions: { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts index c49f8e180c..d08e01a230 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts @@ -1,30 +1,44 @@ -import { UmbDataTypeTreeStore, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './data-type.tree.store'; -import { UmbDataTypeServerDataSource } from './sources/data-type.server.data'; +import { UmbDataTypeTreeServerDataSource } from './sources/data-type.tree.server.data'; import { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from './data-type.store'; -import { DataTypeTreeServerDataSource } from './sources/data-type.tree.server.data'; -import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbDataTypeServerDataSource } from './sources/data-type.server.data'; +import { UmbDataTypeTreeStore, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './data-type.tree.store'; +import { UmbDataTypeFolderServerDataSource } from './sources/data-type-folder.server.data'; +import type { + UmbTreeDataSource, + UmbTreeRepository, + UmbDetailRepository, + UmbFolderDataSource, + UmbDataSource, +} from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { ProblemDetailsModel, DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + CreateDataTypeRequestModel, + CreateFolderRequestModel, + DataTypeResponseModel, + FolderModelBaseModel, + FolderTreeItemResponseModel, + UpdateDataTypeRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; +import { UmbFolderRepository } from '@umbraco-cms/backoffice/repository'; type ItemType = DataTypeResponseModel; type TreeItemType = any; -// 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 UmbDataTypeRepository implements UmbTreeRepository, UmbDetailRepository { +export class UmbDataTypeRepository + implements UmbTreeRepository, UmbDetailRepository, UmbFolderRepository +{ #init!: Promise; #host: UmbControllerHostElement; #treeSource: UmbTreeDataSource; - #treeStore?: UmbDataTypeTreeStore; + #detailSource: UmbDataSource; + #folderSource: UmbFolderDataSource; - #detailDataSource: UmbDataTypeServerDataSource; #detailStore?: UmbDataTypeStore; + #treeStore?: UmbDataTypeTreeStore; #notificationContext?: UmbNotificationContext; @@ -32,8 +46,9 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new DataTypeTreeServerDataSource(this.#host); - this.#detailDataSource = new UmbDataTypeServerDataSource(this.#host); + this.#treeSource = new UmbDataTypeTreeServerDataSource(this.#host); + this.#detailSource = new UmbDataTypeServerDataSource(this.#host); + this.#folderSource = new UmbDataTypeFolderServerDataSource(this.#host); this.#init = Promise.all([ new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { @@ -50,9 +65,6 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U ]); } - // TODO: Trash - // TODO: Move - async requestRootTreeItems() { await this.#init; @@ -66,13 +78,9 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U } async requestTreeItemsOf(parentKey: string | null) { + if (!parentKey) throw new Error('Parent key is missing'); 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) { @@ -83,13 +91,9 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U } async requestTreeItems(keys: Array) { + if (!keys) throw new Error('Keys are missing'); 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) }; @@ -101,6 +105,7 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U } async treeItemsOf(parentKey: string | null) { + if (parentKey === undefined) throw new Error('Parent key is missing'); await this.#init; return this.#treeStore!.childrenOf(parentKey); } @@ -113,25 +118,17 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U // DETAILS: async createScaffold(parentKey: string | null) { + if (parentKey === undefined) throw new Error('Parent key is missing'); await this.#init; - if (!parentKey) { - throw new Error('Parent key is missing'); - } - - return this.#detailDataSource.createScaffold(parentKey); + return this.#detailSource.createScaffold(parentKey); } async requestByKey(key: string) { + if (!key) throw new Error('Key is missing'); 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.#detailDataSource.get(key); + const { data, error } = await this.#detailSource.get(key); if (data) { this.#detailStore?.append(data); @@ -141,53 +138,49 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U } async byKey(key: string) { + if (!key) throw new Error('Key is missing'); await this.#init; return this.#detailStore!.byKey(key); } // Could potentially be general methods: + async create(dataType: ItemType) { + if (!dataType) throw new Error('Data Type is missing'); + if (!dataType.key) throw new Error('Data Type key is missing'); - async create(template: ItemType) { await this.#init; - if (!template || !template.key) { - throw new Error('Template is missing'); - } - - const { error } = await this.#detailDataSource.insert(template); + const { error } = await this.#detailSource.insert(dataType); if (!error) { - const notification = { data: { message: `Document created` } }; + const notification = { data: { message: `Data Type created` } }; this.#notificationContext?.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? + this.#detailStore?.append(dataType); + this.#treeStore?.appendItems([dataType]); + } return { error }; } - async save(item: ItemType) { + async save(dataType: ItemType) { + if (!dataType) throw new Error('Data Type is missing'); + if (!dataType.key) throw new Error('Data Type key is missing'); + await this.#init; - if (!item || !item.key) { - throw new Error('Document-Type is missing'); - } - - const { error } = await this.#detailDataSource.update(item); + const { error } = await this.#detailSource.update(dataType.key, dataType); if (!error) { - const notification = { data: { message: `Document saved` } }; + const notification = { data: { message: `Data Type saved` } }; this.#notificationContext?.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(item); - this.#treeStore?.updateItem(item.key, { name: item.name }); + this.#detailStore?.append(dataType); + this.#treeStore?.updateItem(dataType.key, { name: dataType.name }); // TODO: would be nice to align the stores on methods/methodNames. return { error }; @@ -196,16 +189,13 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U // General: async delete(key: string) { + if (!key) throw new Error('Data Type key is missing'); await this.#init; - if (!key) { - throw new Error('Document key is missing'); - } - - const { error } = await this.#detailDataSource.delete(key); + const { error } = await this.#detailSource.delete(key); if (!error) { - const notification = { data: { message: `Document deleted` } }; + const notification = { data: { message: `Data Type deleted` } }; this.#notificationContext?.peek('positive', notification); } @@ -218,4 +208,74 @@ export class UmbDataTypeRepository implements UmbTreeRepository, U return { error }; } + + // folder + async createFolderScaffold(parentKey: string | null) { + if (parentKey === undefined) throw new Error('Parent key is missing'); + return this.#folderSource.createScaffold(parentKey); + } + + // TODO: temp create type until backend is ready. Remove the key addition when new types are generated. + async createFolder(folderRequest: CreateFolderRequestModel & { key?: string | undefined }) { + if (!folderRequest) throw new Error('folder request is missing'); + await this.#init; + + const { error } = await this.#folderSource.insert(folderRequest); + + // TODO: We need to push a new item to the tree store to update the tree. How do we want to create the tree items? + if (!error) { + const treeItem: FolderTreeItemResponseModel = { + $type: 'FolderTreeItemResponseModel', + parentKey: folderRequest.parentKey, + name: folderRequest.name, + key: folderRequest.key, + isFolder: true, + isContainer: false, + type: 'data-type', + icon: 'umb:folder', + hasChildren: false, + }; + + this.#treeStore?.appendItems([treeItem]); + } + + return { error }; + } + + async deleteFolder(key: string) { + if (!key) throw new Error('Key is missing'); + + const { error } = await this.#folderSource.delete(key); + + if (!error) { + this.#treeStore?.removeItem(key); + } + + return { error }; + } + + async updateFolder(key: string, folder: FolderModelBaseModel) { + if (!key) throw new Error('Key is missing'); + if (!folder) throw new Error('Folder data is missing'); + + const { error } = await this.#folderSource.update(key, folder); + + if (!error) { + this.#treeStore?.updateItem(key, { name: folder.name }); + } + + return { error }; + } + + async requestFolder(key: string) { + if (!key) throw new Error('Key is missing'); + + const { data, error } = await this.#folderSource.get(key); + + if (data) { + this.#treeStore?.appendItems([data]); + } + + return { data, error }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-folder.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-folder.server.data.ts new file mode 100644 index 0000000000..ddeea07a1a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-folder.server.data.ts @@ -0,0 +1,112 @@ +import { v4 as uuidv4 } from 'uuid'; +import { UmbFolderDataSource } from '@umbraco-cms/backoffice/repository'; +import { + DataTypeResource, + FolderReponseModel, + CreateFolderRequestModel, + FolderModelBaseModel, +} from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for a Data Type folder that fetches data from the server + * @export + * @class UmbDataTypeFolderServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbDataTypeFolderServerDataSource implements UmbFolderDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbDataTypeFolderServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeFolderServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Creates a Data Type folder with the given key from the server + * @param {string} key + * @return {*} + * @memberof UmbDataTypeFolderServerDataSource + */ + async createScaffold(parentKey: string | null) { + const scaffold: FolderReponseModel = { + $type: 'FolderReponseModel', + name: '', + key: uuidv4(), + parentKey, + }; + + return { data: scaffold }; + } + + /** + * Fetches a Data Type folder with the given key from the server + * @param {string} key + * @return {*} + * @memberof UmbDataTypeFolderServerDataSource + */ + async get(key: string) { + if (!key) throw new Error('Key is missing'); + return tryExecuteAndNotify( + this.#host, + DataTypeResource.getDataTypeFolderByKey({ + key, + }) + ); + } + + /** + * Inserts a new Data Type folder on the server + * @param {folder} folder + * @return {*} + * @memberof UmbDataTypeFolderServerDataSource + */ + async insert(folder: CreateFolderRequestModel) { + if (!folder) throw new Error('Folder is missing'); + return tryExecuteAndNotify( + this.#host, + DataTypeResource.postDataTypeFolder({ + requestBody: folder, + }) + ); + } + + /** + * Updates a Data Type folder on the server + * @param {folder} folder + * @return {*} + * @memberof UmbDataTypeFolderServerDataSource + */ + async update(key: string, folder: FolderModelBaseModel) { + if (!key) throw new Error('Key is missing'); + if (!key) throw new Error('Folder data is missing'); + return tryExecuteAndNotify( + this.#host, + DataTypeResource.putDataTypeFolderByKey({ + key, + requestBody: folder, + }) + ); + } + + /** + * Deletes a Data Type folder with the given key on the server + * @param {string} key + * @return {*} + * @memberof UmbDataTypeServerDataSource + */ + async delete(key: string) { + if (!key) throw new Error('Key is missing'); + return tryExecuteAndNotify( + this.#host, + DataTypeResource.deleteDataTypeFolderByKey({ + key, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts index 126a1bd5ec..aab943d402 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts @@ -1,10 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; import { - ProblemDetailsModel, DataTypeResource, DataTypeResponseModel, DataTypeModelBaseModel, + CreateDataTypeRequestModel, + UpdateDataTypeRequestModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -15,7 +16,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDataTypeServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDataTypeServerDataSource implements UmbDataSource { +export class UmbDataTypeServerDataSource + implements + UmbDataSource +{ #host: UmbControllerHostElement; /** @@ -34,11 +38,7 @@ export class UmbDataTypeServerDataSource implements UmbDataSource( + tryExecuteAndNotify( this.#host, - // TODO: avoid this any?.. - tryExecuteAndNotify( - this.#host, - DataTypeResource.postDataType({ - requestBody, - }) - ) as any + DataTypeResource.postDataType({ + requestBody: dataType, + }) ); } @@ -95,42 +87,14 @@ export class UmbDataTypeServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, DataTypeResource.putDataTypeByKey({ - key: dataType.key, - requestBody, - }) - ); - } - - /** - * Trash a Document on the server - * @param {Document} Document - * @return {*} - * @memberof UmbDataTypeServerDataSource - */ - async trash(key: string) { - if (!key) { - const error: ProblemDetailsModel = { title: 'DataType key is missing' }; - return { error }; - } - - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( - this.#host, - DataTypeResource.deleteDataTypeByKey({ key, + requestBody: data, }) ); } @@ -142,13 +106,9 @@ export class UmbDataTypeServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, DataTypeResource.deleteDataTypeByKey({ key, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts index 8613494a9d..c2582f00e8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.tree.server.data.ts @@ -1,54 +1,21 @@ import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; -import { ProblemDetailsModel, DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; +import { DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Document tree that fetches data from the server * @export - * @class DocumentTreeServerDataSource + * @class UmbDataTypeTreeServerDataSource * @implements {DocumentTreeDataSource} */ -export class DataTypeTreeServerDataSource implements UmbTreeDataSource { +export class UmbDataTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; - // TODO: how do we handle trashed items? - async trashItems(keys: Array) { - if (!keys) { - const error: ProblemDetailsModel = { title: 'DataType keys is missing' }; - return { error }; - } - - // TODO: use resources when end point is ready: - /* - return tryExecuteAndNotify( - this.#host, - DataTypeResource.deleteDataTypeByKey({ - key: keys, - }) - ); - */ - return Promise.resolve({ error: null, data: null }); - } - - async moveItems(keys: Array, destination: string) { - // TODO: use backend cli when available. - return tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/data-type/move', { - method: 'POST', - body: JSON.stringify({ keys, destination }), - headers: { - 'Content-Type': 'application/json', - }, - }) - ); - } - /** - * Creates an instance of DocumentTreeServerDataSource. + * Creates an instance of UmbDataTypeTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof DocumentTreeServerDataSource + * @memberof UmbDataTypeTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -57,7 +24,7 @@ export class DataTypeTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDataTypeTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, DataTypeResource.getTreeDataTypeRoot({})); @@ -67,13 +34,10 @@ export class DataTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent key from the server * @param {(string | null)} parentKey * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDataTypeTreeServerDataSource */ async getChildrenOf(parentKey: string | null) { - if (!parentKey) { - const error: ProblemDetailsModel = { title: 'Parent key is missing' }; - return { error }; - } + if (!parentKey) throw new Error('Parent key is missing'); return tryExecuteAndNotify( this.#host, @@ -87,14 +51,10 @@ export class DataTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given keys from the server * @param {Array} keys * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDataTypeTreeServerDataSource */ async getItems(keys: Array) { - if (keys) { - const error: ProblemDetailsModel = { title: 'Keys are missing' }; - return { error }; - } - + if (!keys) throw new Error('Keys are missing'); return tryExecuteAndNotify( this.#host, DataTypeResource.getTreeDataTypeItem({ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts index 768c114d96..d37dada762 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts @@ -105,9 +105,11 @@ export class UmbLanguageRepository { * @memberof UmbLanguageRepository */ async save(language: LanguageResponseModel) { + if (!language.isoCode) throw new Error('Language iso code is missing'); + await this.#init; - const { error } = await this.#dataSource.update(language); + const { error } = await this.#dataSource.update(language.isoCode, language); if (!error) { const notification = { data: { message: `Language saved` } }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts deleted file mode 100644 index 4a3e236af4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LanguageResponseModel, PagedLanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UmbDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -// TODO: This is a temporary solution until we have a proper paging interface -type paging = { - skip: number; - take: number; -}; - -export interface UmbLanguageDataSource extends UmbDataSource { - createScaffold(): Promise>; - get(isoCode: string): Promise>; - delete(isoCode: string): Promise>; - getCollection(paging: paging): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts index a7b8096dc5..af2231bbe1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts @@ -1,6 +1,12 @@ -import { ProblemDetailsModel, LanguageResource, LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + ProblemDetailsModel, + LanguageResource, + LanguageResponseModel, + CreateLanguageRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbDataSource } from '@umbraco-cms/backoffice/repository'; /** * A data source for the Language that fetches data from the server @@ -8,7 +14,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbLanguageServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource { +export class UmbLanguageServerDataSource + implements UmbDataSource +{ #host: UmbControllerHostElement; /** @@ -27,11 +35,7 @@ export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource * @memberof UmbLanguageServerDataSource */ async get(isoCode: string) { - if (!isoCode) { - const error: ProblemDetailsModel = { title: 'Iso Code is missing' }; - return { error }; - } - + if (!isoCode) throw new Error('Iso Code is missing'); return tryExecuteAndNotify( this.#host, LanguageResource.getLanguageByIsoCode({ @@ -64,7 +68,7 @@ export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource * @return {*} * @memberof UmbLanguageServerDataSource */ - async insert(language: LanguageResponseModel) { + async insert(language: CreateLanguageRequestModel) { if (!language.isoCode) { const error: ProblemDetailsModel = { title: 'Language iso code is missing' }; return { error }; @@ -79,7 +83,7 @@ export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource * @return {*} * @memberof UmbLanguageServerDataSource */ - async update(language: LanguageResponseModel) { + async update(iscoCode: string, language: LanguageResponseModel) { if (!language.isoCode) { const error: ProblemDetailsModel = { title: 'Language iso code is missing' }; return { error }; @@ -98,15 +102,8 @@ export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource * @memberof UmbLanguageServerDataSource */ async delete(isoCode: string) { - if (!isoCode) { - const error: ProblemDetailsModel = { title: 'Iso code is missing' }; - return { error }; - } - - return tryExecuteAndNotify( - this.#host, - tryExecuteAndNotify(this.#host, LanguageResource.deleteLanguageByIsoCode({ isoCode })).then(() => isoCode) - ); + if (!isoCode) throw new Error('Iso Code is missing'); + return tryExecuteAndNotify(this.#host, LanguageResource.deleteLanguageByIsoCode({ isoCode })); } /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts index f56abe82ee..e098faaefd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts @@ -166,7 +166,7 @@ export class UmbRelationTypeRepository implements UmbTreeRepository { +export class UmbRelationTypeServerDataSource + implements UmbDataSource +{ #host: UmbControllerHostElement; /** @@ -61,72 +63,35 @@ export class UmbRelationTypeServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, - // TODO: avoid this any?.. - tryExecuteAndNotify( - this.#host, - RelationTypeResource.postRelationType({ - requestBody, - }) - ) as any - ); - } - - /** - * Updates a RelationType on the server - * @param {RelationTypeResponseModel} RelationType - * @return {*} - * @memberof UmbRelationTypeServerDataSource - */ - // TODO: Error mistake in this: - async update(RelationType: RelationTypeResponseModel) { - if (!RelationType.key) { - const error: ProblemDetailsModel = { title: 'RelationType key is missing' }; - return { error }; - } - - const requestBody: UpdateRelationTypeRequestModel = { ...RelationType }; - - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( - this.#host, - RelationTypeResource.putRelationTypeByKey({ - key: RelationType.key, - requestBody, + RelationTypeResource.postRelationType({ + requestBody: relationType, }) ); } /** - * Trash a Document on the server - * @param {Document} Document + * Updates a RelationType on the server + * @param {RelationTypeResponseModel} relationType * @return {*} * @memberof UmbRelationTypeServerDataSource */ - async trash(key: string) { - if (!key) { - const error: ProblemDetailsModel = { title: 'RelationType key is missing' }; - return { error }; - } + async update(key: string, relationType: UpdateRelationTypeRequestModel) { + if (!key) throw new Error('RelationType key is missing'); - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( + return tryExecuteAndNotify( this.#host, - RelationTypeResource.deleteRelationTypeByKey({ + RelationTypeResource.putRelationTypeByKey({ key, + requestBody: relationType, }) ); } @@ -143,8 +108,7 @@ export class UmbRelationTypeServerDataSource implements UmbDataSource( + return tryExecuteAndNotify( this.#host, RelationTypeResource.deleteRelationTypeByKey({ key, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 7ba6b5fb07..d7c74fb3df 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -41,6 +41,8 @@ import './tree/tree.element'; import './tree/entity-tree-item/entity-tree-item.element'; import './tree/tree-menu-item/tree-menu-item.element'; +import './menu/menu-item-base/menu-item-base.element'; + import './variantable-property/variantable-property.element'; import './workspace/workspace-action-menu/workspace-action-menu.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu-item/menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu-item/menu-item.element.ts deleted file mode 100644 index caf7c4d39b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu-item/menu-item.element.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { css, html } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, state } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section/section.context'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extensions-registry'; - -@customElement('umb-menu-item') -export class UmbMenuItemElement extends UmbLitElement { - static styles = [UUITextStyles, css``]; - - @property({ type: Object, attribute: false }) - manifest!: ManifestMenuItem; - - @state() - private _href?: string; - - #sectionContext?: UmbSectionContext; - - constructor() { - super(); - - this.consumeContext(UMB_SECTION_CONTEXT_TOKEN, (sectionContext) => { - this.#sectionContext = sectionContext; - this._observeSection(); - }); - } - - private _observeSection() { - if (!this.#sectionContext) return; - - this.observe(this.#sectionContext?.pathname, (pathname) => { - if (!pathname) return; - this._href = this._constructPath(pathname); - }); - } - - // TODO: how do we handle this? - private _constructPath(sectionPathname: string) { - return `section/${sectionPathname}/workspace/${this.manifest.meta.entityType}`; - } - - render() { - return html` `; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'umb-menu-item': UmbMenuItemElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item-base/menu-item-base.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item-base/menu-item-base.element.ts new file mode 100644 index 0000000000..ad7cd66ff4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item-base/menu-item-base.element.ts @@ -0,0 +1,125 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { map } from 'rxjs'; +import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../../section/section.context'; +import { + UmbSectionSidebarContext, + UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, +} from '../../section/section-sidebar/section-sidebar.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { ManifestEntityAction } from '@umbraco-cms/backoffice/extensions-registry'; +import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-menu-item-base') +export class UmbMenuItemBaseElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + private _entityType?: string; + @property({ type: String, attribute: 'entity-type' }) + public get entityType() { + return this._entityType; + } + public set entityType(value: string | undefined) { + this._entityType = value; + this.#observeEntityActions(); + } + + @property({ type: String, attribute: 'icon-name' }) + public iconName = ''; + + @property({ type: String }) + public label = ''; + + @property({ type: Boolean, attribute: 'has-children' }) + public hasChildren = false; + + @state() + private _href?: string; + + @state() + private _hasActions = false; + + #sectionContext?: UmbSectionContext; + #sectionSidebarContext?: UmbSectionSidebarContext; + #actionObserver?: UmbObserverController>; + + constructor() { + super(); + + this.consumeContext(UMB_SECTION_CONTEXT_TOKEN, (sectionContext) => { + this.#sectionContext = sectionContext; + this._observeSection(); + }); + + this.consumeContext(UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, (sectionContext) => { + this.#sectionSidebarContext = sectionContext; + }); + } + + #observeEntityActions() { + if (this.#actionObserver) this.#actionObserver.destroy(); + + this.#actionObserver = this.observe( + umbExtensionsRegistry + .extensionsOfType('entityAction') + .pipe(map((actions) => actions.filter((action) => action.conditions.entityType === this.entityType))), + (actions) => { + this._hasActions = actions.length > 0; + }, + 'entityAction' + ); + } + + private _observeSection() { + if (!this.#sectionContext) return; + + this.observe(this.#sectionContext?.pathname, (pathname) => { + if (!pathname) return; + this._href = this._constructPath(pathname); + }); + } + + // TODO: how do we handle this? + // TODO: use router context + private _constructPath(sectionPathname: string) { + return `section/${sectionPathname}/workspace/${this.entityType}`; + } + + private _openActions() { + if (!this.entityType) throw new Error('Entity type is not defined'); + this.#sectionSidebarContext?.toggleContextMenu(this.entityType, undefined, this.label); + } + + render() { + return html` ${this.#renderIcon()}${this.#renderActions()}`; + } + + #renderIcon() { + return html` `; + } + + #renderActions() { + return html` + ${this._hasActions + ? html` + + + + + + ` + : nothing} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-menu-item-base': UmbMenuItemBaseElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item/menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item/menu-item.element.ts new file mode 100644 index 0000000000..0639f05620 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu-item/menu-item.element.ts @@ -0,0 +1,31 @@ +import { html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extensions-registry'; + +export interface UmbMenuItemExtensionElement { + manifest: ManifestMenuItem; +} + +@customElement('umb-menu-item') +export class UmbMenuItemElement extends UmbLitElement implements UmbMenuItemExtensionElement { + static styles = [UUITextStyles]; + + @property({ type: Object, attribute: false }) + manifest!: ManifestMenuItem; + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-menu-item': UmbMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu.element.ts index 62cc9d6c76..55679c7bc2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/menu/menu.element.ts @@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js'; import { ManifestMenu, ManifestMenuItem } from '@umbraco-cms/backoffice/extensions-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import '../menu-item/menu-item.element'; +import './menu-item/menu-item.element'; @customElement('umb-menu') export class UmbMenuElement extends UmbLitElement { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts index ccede9a33e..c3a4264b2a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts @@ -20,13 +20,14 @@ export class UmbSectionSidebarContext { this.#host = host; } - toggleContextMenu(entityType: string, unique: string, headline: string) { - this.#unique.getValue() === unique ? this.closeContextMenu() : this.openContextMenu(entityType, unique, headline); + toggleContextMenu(entityType: string, unique: string | undefined, headline: string) { + console.log('open for ', entityType, unique, headline); + this.openContextMenu(entityType, unique, headline); } // TODO: we wont get notified about tree item name changes because we don't have a subscription // we need to figure out how we best can handle this when we only know the entity and unique id - openContextMenu(entityType: string, unique: string, headline: string) { + openContextMenu(entityType: string, unique: string | undefined, headline: string) { this.#entityType.next(entityType); this.#unique.next(unique); this.#headline.next(headline); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-menu-item/tree-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-menu-item/tree-menu-item.element.ts index 91a0304a92..7922feb209 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-menu-item/tree-menu-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-menu-item/tree-menu-item.element.ts @@ -1,5 +1,7 @@ import { html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UUIMenuItemEvent } from '@umbraco-ui/uui'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { ManifestKind, ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extensions-registry'; @@ -22,11 +24,13 @@ export class UmbMenuItemTreeElement extends UmbLitElement { @state() private _renderTree = false; - private _onShowChildren() { + private _onShowChildren(event: UUIMenuItemEvent) { + event.stopPropagation(); this._renderTree = true; } - private _onHideChildren() { + private _onHideChildren(event: UUIMenuItemEvent) { + event.stopPropagation(); this._renderTree = false; } @@ -38,15 +42,15 @@ export class UmbMenuItemTreeElement extends UmbLitElement { render() { return this.manifest ? html` - + has-children> ${this._renderTree ? html`` : nothing} - + ` : ''; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/folder/folder-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/folder/folder-modal.element.ts new file mode 100644 index 0000000000..eb9986ff11 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/folder/folder-modal.element.ts @@ -0,0 +1,177 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { UmbFolderModalData, UmbFolderModalResult, UmbModalHandler } from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UmbFolderRepository } from '@umbraco-cms/backoffice/repository'; +import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { FolderReponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { ManifestBase } from '@umbraco-cms/backoffice/extensions-registry'; + +@customElement('umb-folder-modal') +export class UmbFolderModalElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #name { + width: 100%; + } + `, + ]; + + @property({ attribute: false }) + modalHandler?: UmbModalHandler; + + private _data?: UmbFolderModalData; + @property({ type: Object, attribute: false }) + public get data() { + return this._data; + } + public set data(value: UmbFolderModalData | undefined) { + this._data = value; + this.#unique = value?.unique || null; + this.#repositoryAlias = value?.repositoryAlias; + this.#observeRepository(); + } + + #repositoryAlias?: string; + #unique: string | null = null; + #repository?: UmbFolderRepository; + #repositoryObserver?: UmbObserverController; + + @state() + _folder?: FolderReponseModel; + + @state() + _headline?: string; + + @state() + _isNew = false; + + #observeRepository() { + this.#repositoryObserver?.destroy(); + if (!this.#repositoryAlias) return; + this.#repositoryObserver = this.observe( + umbExtensionsRegistry.getByTypeAndAlias('repository', this.#repositoryAlias), + async (repositoryManifest) => { + if (!repositoryManifest) return; + + try { + const result = await createExtensionClass(repositoryManifest, [this]); + this.#repository = result; + this.#init(); + } catch (error) { + throw new Error('Could not create repository with alias: ' + this.#repositoryAlias + ''); + } + } + ); + } + + // TODO: so I ended up building a full workspace in the end. We should look into building the real workspace folder editor + // and see if we can use that in this modal instead of this custom logic. + #init() { + if (this.#unique) { + this.#load(); + } else { + this.#create(); + } + } + + async #create() { + if (!this.#repository) throw new Error('Repository is required to create folder'); + const { data } = await this.#repository.createFolderScaffold(this.#unique); + this._folder = data; + this._isNew = true; + } + + async #load() { + if (!this.#unique) throw new Error('Unique is required to load folder'); + if (!this.#repository) throw new Error('Repository is required to create folder'); + const { data } = await this.#repository.requestFolder(this.#unique); + this._folder = data; + this._isNew = false; + } + + @query('#dataTypeFolderForm') + private _formElement?: HTMLFormElement; + + #onCancel() { + this.modalHandler?.reject(); + } + + #submitForm() { + this._formElement?.requestSubmit(); + } + + async #onSubmit(event: SubmitEvent) { + event.preventDefault(); + if (!this._folder) throw new Error('Folder is not initialized correctly'); + if (!this.#repository) throw new Error('Repository is required to create folder'); + + const isValid = this._formElement?.checkValidity(); + if (!isValid) return; + + let error: ProblemDetailsModel | undefined; + + const formData = new FormData(this._formElement); + const folderName = formData.get('name') as string; + + this._folder = { ...this._folder, name: folderName }; + + if (this._isNew) { + const { error: createError } = await this.#repository.createFolder(this._folder); + error = createError; + } else { + if (!this.#unique) throw new Error('Unique is required to update folder'); + const { error: updateError } = await this.#repository.updateFolder(this.#unique, this._folder); + error = updateError; + } + + if (!error) { + this.modalHandler?.submit(); + } + } + + render() { + return html` + + + +
+ + Folder name + + +
+
+
+ + + +
+ `; + } +} + +export default UmbFolderModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-folder-modal': UmbFolderModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts index 37134949c7..a91cbbc426 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts @@ -7,6 +7,12 @@ const modals: Array = [ name: 'Confirm Modal', loader: () => import('./confirm/confirm-modal.element'), }, + { + type: 'modal', + alias: 'Umb.Modal.Folder', + name: 'Folder Modal', + loader: () => import('./folder/folder-modal.element'), + }, { type: 'modal', alias: 'Umb.Modal.IconPicker', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts index 65fcd64e91..1e29622343 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/repository/sources/stylesheet.server.data.ts @@ -8,7 +8,7 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; * @class UmbStylesheetServerDataSource * @implements {UmbStylesheetServerDataSource} */ -export class UmbStylesheetServerDataSource implements UmbDataSource { +export class UmbStylesheetServerDataSource implements UmbDataSource { #host: UmbControllerHostElement; /** @@ -38,10 +38,10 @@ export class UmbStylesheetServerDataSource implements UmbDataSource> { throw new Error('Method not implemented.'); } - update(data: StylesheetDetails): Promise> { + update(path: string, data: StylesheetDetails): Promise> { throw new Error('Method not implemented.'); } - delete(key: string): Promise> { + delete(path: string): Promise { throw new Error('Method not implemented.'); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index 8661c38826..83e6cd42e3 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -1,9 +1,21 @@ import { UmbEntityData } from './entity.data'; import { createFolderTreeItem } from './utils'; -import type { FolderTreeItemResponseModel, DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import type { + FolderTreeItemResponseModel, + DataTypeResponseModel, + CreateFolderRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; -// TODO: investigate why we don't get an entity type as part of the DataTypeModel -export const data: Array = [ +// TODO: investigate why we don't get an type as part of the DataTypeModel +export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | FolderTreeItemResponseModel> = [ + { + $type: 'data-type', + type: 'data-type', + name: 'Folder 1', + key: 'dt-folder1', + parentKey: null, + isFolder: true, + }, { $type: 'data-type', type: 'data-type', @@ -590,7 +602,7 @@ export const data: Array = [ // TODO: all properties are optional in the server schema. I don't think this is correct. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -class UmbDataTypeData extends UmbEntityData { +class UmbDataTypeData extends UmbEntityData { constructor() { super(data); } @@ -609,6 +621,28 @@ class UmbDataTypeData extends UmbEntityData { const items = this.data.filter((item) => keys.includes(item.key ?? '')); return items.map((item) => createFolderTreeItem(item)); } + + createFolder(folder: CreateFolderRequestModel & { key: string | undefined }) { + const newFolder: FolderTreeItemResponseModel = { + name: folder.name, + key: folder.key, + parentKey: folder.parentKey, + $type: 'data-type', + type: 'data-type', + isFolder: true, + isContainer: false, + }; + + this.data.push(newFolder); + } + + // TODO: this could be reused across other types that support folders + deleteFolder(key: string) { + const item = this.getByKey(key) as FolderTreeItemResponseModel; + if (!item) throw new Error(`Item with key ${key} not found`); + if (!item.isFolder) throw new Error(`Item with key ${key} is not a folder`); + this.data = this.data.filter((item) => item.key !== key); + } } export const umbDataTypeData = new UmbDataTypeData(); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type.handlers.ts index c6c3c740e2..d235482f9a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type.handlers.ts @@ -1,19 +1,12 @@ import { rest } from 'msw'; import { umbDataTypeData } from '../data/data-type.data'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; // TODO: add schema export const handlers = [ - rest.delete('/umbraco/backoffice/data-type/:key', async (req, res, ctx) => { - const key = req.params.key as string; - if (!key) return; - - umbDataTypeData.delete([key]); - - return res(ctx.status(200)); - }), - - rest.get('/umbraco/management/api/v1/tree/data-type/root', (req, res, ctx) => { + // TREE + rest.get(umbracoPath('/tree/data-type/root'), (req, res, ctx) => { const rootItems = umbDataTypeData.getTreeRoot(); const response = { total: rootItems.length, @@ -22,7 +15,7 @@ export const handlers = [ return res(ctx.status(200), ctx.json(response)); }), - rest.get('/umbraco/management/api/v1/tree/data-type/children', (req, res, ctx) => { + rest.get(umbracoPath('/tree/data-type/children'), (req, res, ctx) => { const parentKey = req.url.searchParams.get('parentKey'); if (!parentKey) return; @@ -36,13 +29,70 @@ export const handlers = [ return res(ctx.status(200), ctx.json(response)); }), - rest.get('/umbraco/management/api/v1/tree/data-type/item', (req, res, ctx) => { + rest.get(umbracoPath('/tree/data-type/item'), (req, res, ctx) => { const keys = req.url.searchParams.getAll('key'); if (!keys) return; const items = umbDataTypeData.getTreeItem(keys); return res(ctx.status(200), ctx.json(items)); }), + // FOLDERS + rest.post(umbracoPath('/data-type/folder'), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbDataTypeData.createFolder(data); + + return res(ctx.status(200)); + }), + + rest.get(umbracoPath('/data-type/folder/:key'), (req, res, ctx) => { + const key = req.params.key as string; + if (!key) return; + + const dataType = umbDataTypeData.getByKey(key); + + return res(ctx.status(200), ctx.json(dataType)); + }), + + rest.put(umbracoPath('/data-type/folder/:key'), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbDataTypeData.save(data); + + return res(ctx.status(200)); + }), + + rest.delete(umbracoPath('/data-type/folder/:key'), async (req, res, ctx) => { + const key = req.params.key as string; + if (!key) return; + + try { + umbDataTypeData.deleteFolder(key); + return res(ctx.status(200)); + } catch (error) { + return res( + ctx.status(404), + ctx.json({ + status: 404, + type: 'error', + detail: 'Not Found', + }) + ); + } + }), + + // Details + rest.post(umbracoPath('/data-type'), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + const saved = umbDataTypeData.save(data); + + return res(ctx.status(200), ctx.json(saved)); + }), + rest.get(umbracoPath('/data-type/:key'), (req, res, ctx) => { const key = req.params.key as string; if (!key) return; @@ -52,15 +102,6 @@ export const handlers = [ return res(ctx.status(200), ctx.json(dataType)); }), - rest.post(umbracoPath('/data-type/:key'), async (req, res, ctx) => { - const data = await req.json(); - if (!data) return; - - const saved = umbDataTypeData.save(data); - - return res(ctx.status(200), ctx.json(saved)); - }), - rest.put(umbracoPath('/data-type/:key'), async (req, res, ctx) => { const data = await req.json(); if (!data) return; @@ -69,4 +110,13 @@ export const handlers = [ return res(ctx.status(200), ctx.json(saved)); }), + + rest.delete(umbracoPath('/data-type/:key'), async (req, res, ctx) => { + const key = req.params.key as string; + if (!key) return; + + umbDataTypeData.delete([key]); + + return res(ctx.status(200)); + }), ];