diff --git a/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts new file mode 100644 index 0000000000..f9621e0c30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts @@ -0,0 +1,5 @@ +import { UmbRepositoryResponse } from './detail-repository.interface'; + +export interface UmbCopyRepository { + copy(unique: string, targetUnique: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts new file mode 100644 index 0000000000..b9d41d7971 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts @@ -0,0 +1,5 @@ +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbCopyDataSource { + copy(unique: string, targetUnique: 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 3851c96686..08d765462a 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 @@ -4,3 +4,4 @@ export * from './folder-data-source.interface'; export * from './tree-data-source.interface'; export * from './item-data-source.interface'; export * from './move-data-source.interface'; +export * from './copy-data-source.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/index.ts index 0c73ef0870..ba407c2083 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/index.ts @@ -4,3 +4,4 @@ export * from './tree-repository.interface'; export * from './folder-repository.interface'; export * from './item-repository.interface'; export * from './move-repository.interface'; +export * from './copy-repository.interface'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts new file mode 100644 index 0000000000..9b2fd19b53 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts @@ -0,0 +1,27 @@ +import { UmbDataTypeRepository } from '../../repository/data-type.repository'; +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; + +// TODO: investigate what we need to make a generic copy action +export class UmbCopyDataTypeEntityAction 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.#modalContext) throw new Error('Modal context is not available'); + if (!this.repository) throw new Error('Repository is not available'); + + const modalHandler = this.#modalContext?.open(UMB_DATA_TYPE_PICKER_MODAL); + const { selection } = await modalHandler.onSubmit(); + await this.repository.copy(this.unique, selection[0]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts new file mode 100644 index 0000000000..726637162b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts @@ -0,0 +1,24 @@ +import { DATA_TYPE_ENTITY_TYPE } from '../..'; +import { DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/manifests'; +import { UmbCopyDataTypeEntityAction } from './copy.action'; +import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.Copy', + name: 'Copy Data Type Entity Action', + weight: 900, + meta: { + icon: 'umb:documents', + label: 'Copy to...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbCopyDataTypeEntityAction, + }, + conditions: { + entityType: DATA_TYPE_ENTITY_TYPE, + }, + }, +]; + +export const manifests = [...entityActions]; 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 index eb57a2b500..64d90c4142 100644 --- 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 @@ -2,6 +2,8 @@ import { DATA_TYPE_ENTITY_TYPE } from '..'; import { DATA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; import { manifests as createManifests } from './create/manifests'; import { manifests as moveManifests } from './move/manifests'; +import { manifests as copyManifests } from './copy/manifests'; + import { UmbDeleteEntityAction, UmbDeleteFolderEntityAction, @@ -57,4 +59,4 @@ const entityActions: Array = [ }, ]; -export const manifests = [...entityActions, ...createManifests, ...moveManifests]; +export const manifests = [...entityActions, ...createManifests, ...moveManifests, ...copyManifests]; 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 52a24e2a58..fb2608ef19 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 @@ -6,12 +6,14 @@ import { UmbDataTypeTreeStore, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './ import { UmbDataTypeFolderServerDataSource } from './sources/data-type-folder.server.data'; import { UmbDataTypeItemServerDataSource } from './sources/data-type-item.server.data'; import { UMB_DATA_TYPE_ITEM_STORE_CONTEXT_TOKEN, UmbDataTypeItemStore } from './data-type-item.store'; +import { UmbDataTypeCopyServerDataSource } from './sources/data-type-copy.server.data'; import type { UmbTreeRepository, UmbDetailRepository, UmbItemRepository, UmbFolderRepository, UmbMoveRepository, + UmbCopyRepository, } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; @@ -25,14 +27,14 @@ import { UpdateDataTypeRequestModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; - export class UmbDataTypeRepository implements UmbItemRepository, UmbTreeRepository, UmbDetailRepository, UmbFolderRepository, - UmbMoveRepository + UmbMoveRepository, + UmbCopyRepository { #init: Promise; @@ -43,6 +45,7 @@ export class UmbDataTypeRepository #folderSource: UmbDataTypeFolderServerDataSource; #itemSource: UmbDataTypeItemServerDataSource; #moveSource: UmbDataTypeMoveServerDataSource; + #copySource: UmbDataTypeCopyServerDataSource; #detailStore?: UmbDataTypeStore; #treeStore?: UmbDataTypeTreeStore; @@ -59,6 +62,7 @@ export class UmbDataTypeRepository this.#folderSource = new UmbDataTypeFolderServerDataSource(this.#host); this.#itemSource = new UmbDataTypeItemServerDataSource(this.#host); this.#moveSource = new UmbDataTypeMoveServerDataSource(this.#host); + this.#copySource = new UmbDataTypeCopyServerDataSource(this.#host); this.#init = Promise.all([ new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, (instance) => { @@ -304,6 +308,24 @@ export class UmbDataTypeRepository return { error }; } + + async copy(id: string, targetId: string) { + await this.#init; + const { data: dataTypeCopyId, error } = await this.#copySource.copy(id, targetId); + if (error) return { error }; + + if (dataTypeCopyId) { + const { data: dataTypeCopy } = await this.requestById(dataTypeCopyId); + if (!dataTypeCopy) throw new Error('Could not find copied data type'); + this.#treeStore?.appendItems([dataTypeCopy]); + this.#treeStore?.updateItem(targetId, { hasChildren: true }); + + const notification = { data: { message: `Data type copied` } }; + this.#notificationContext?.peek('positive', notification); + } + + return { data: dataTypeCopyId }; + } } export const createTreeItem = (item: CreateDataTypeRequestModel): FolderTreeItemResponseModel => { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts new file mode 100644 index 0000000000..d322063e48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts @@ -0,0 +1,43 @@ +import { DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbCopyDataSource } from '@umbraco-cms/backoffice/repository'; + +/** + * A data source for Data Type items that fetches data from the server + * @export + * @class UmbDataTypeCopyServerDataSource + */ +export class UmbDataTypeCopyServerDataSource implements UmbCopyDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbDataTypeCopyServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeCopyServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Copy an item for the given id to the target id + * @param {Array} id + * @return {*} + * @memberof UmbDataTypeCopyServerDataSource + */ + async copy(id: string, targetId: string) { + if (!id) throw new Error('Id is missing'); + if (!targetId) throw new Error('Target Id is missing'); + + return tryExecuteAndNotify( + this.#host, + DataTypeResource.postDataTypeByIdCopy({ + id, + requestBody: { + targetId, + }, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts index 19f132d492..9eb9ba8be8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts @@ -1,3 +1,4 @@ +import { v4 as uuid } from 'uuid'; import { UmbData } from './data'; import type { Entity } from '@umbraco-cms/backoffice/models'; @@ -60,6 +61,30 @@ export class UmbEntityData extends UmbData { this.updateData(destinationItem); } + copy(ids: Array, destinationKey: string) { + const destinationItem = this.getById(destinationKey); + if (!destinationItem) throw new Error(`Destination item with key ${destinationKey} not found`); + + // TODO: Notice we don't add numbers to the 'copy' name. + const items = this.getByIds(ids); + const copyItems = items.map((item) => { + return { + ...item, + name: item.name + ' Copy', + id: uuid(), + parentId: destinationKey, + }; + }); + + copyItems.forEach((copyItem) => this.insert(copyItem)); + const newIds = copyItems.map((item) => item.id); + + destinationItem.hasChildren = true; + this.updateData(destinationItem); + + return newIds; + } + trash(ids: Array) { const trashedItems: Array = []; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts new file mode 100644 index 0000000000..1d5b764699 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts @@ -0,0 +1,18 @@ +import { rest } from 'msw'; +import { umbDataTypeData } from '../../data/data-type.data'; +import { slug } from './slug'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const copyHandlers = [ + rest.post(umbracoPath(`${slug}/:id/copy`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + + const data = await req.json(); + if (!data) return; + + const newIds = umbDataTypeData.copy([id], data.targetId); + + return res(ctx.status(200), ctx.set({ Location: newIds[0] })); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts index 3cd1dc2d02..706757b33e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts @@ -3,5 +3,13 @@ import { treeHandlers } from './tree.handlers'; import { detailHandlers } from './detail.handlers'; import { itemHandlers } from './item.handlers'; import { moveHandlers } from './move.handlers'; +import { copyHandlers } from './copy.handlers'; -export const handlers = [...treeHandlers, ...itemHandlers, ...folderHandlers, ...moveHandlers, ...detailHandlers]; +export const handlers = [ + ...treeHandlers, + ...itemHandlers, + ...folderHandlers, + ...moveHandlers, + ...copyHandlers, + ...detailHandlers, +];