diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 17530f54c685..675909459358 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -510,6 +510,7 @@ export default { confirmremoveusageof: 'Are you sure you want to remove the usage of <strong>%0%</strong>', confirmlogout: 'Are you sure?', confirmSure: 'Are you sure?', + confirmTrash: (name: string) => `Are you sure you want to move <strong>${name}</strong> to the Recycle Bin?`, cut: 'Cut', editDictionary: 'Edit dictionary item', editLanguage: 'Edit language', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts index 1bda4e17bce8..9db1a3a2cc93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts @@ -5,17 +5,19 @@ import { UUIRefNodeElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-ref-item') export class UmbRefItemElement extends UmbElementMixin(UUIRefNodeElement) { @property({ type: String }) - icon = ''; + icon? = ''; #iconElement = document.createElement('umb-icon'); protected override firstUpdated(_changedProperties: PropertyValues): void { super.firstUpdated(_changedProperties); - // Temporary fix for the icon appending, this could in the future be changed to override a renderIcon method, or other ways to make this happen without appending children. - this.#iconElement.setAttribute('slot', 'icon'); - this.#iconElement.setAttribute('name', this.icon); - this.appendChild(this.#iconElement); + if (this.icon) { + // Temporary fix for the icon appending, this could in the future be changed to override a renderIcon method, or other ways to make this happen without appending children. + this.#iconElement.setAttribute('slot', 'icon'); + this.#iconElement.setAttribute('name', this.icon); + this.appendChild(this.#iconElement); + } } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/constants.ts index f8cd69a673b3..7dcb65f74574 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/constants.ts @@ -1 +1,2 @@ export * from './create/constants.js'; +export * from './delete/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/constants.ts new file mode 100644 index 000000000000..14e2c4886c60 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/constants.ts @@ -0,0 +1 @@ +export { UMB_ENTITY_ACTION_DELETE_KIND_MANIFEST } from './delete.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts index 07995746e6fe..e0474309fa8a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts @@ -1,7 +1,7 @@ import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: UmbExtensionManifestKind = { +export const UMB_ENTITY_ACTION_DELETE_KIND_MANIFEST: UmbExtensionManifestKind = { type: 'kind', alias: 'Umb.Kind.EntityAction.Delete', matchKind: 'delete', @@ -22,3 +22,5 @@ export const manifest: UmbExtensionManifestKind = { }, }, }; + +export const manifest = UMB_ENTITY_ACTION_DELETE_KIND_MANIFEST; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts index 0fadb1aa7867..4fa014b07371 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts @@ -7,22 +7,30 @@ import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backof import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; -export class UmbDeleteEntityAction extends UmbEntityActionBase<MetaEntityActionDeleteKind> { +export class UmbDeleteEntityAction< + MetaKind extends MetaEntityActionDeleteKind = MetaEntityActionDeleteKind, +> extends UmbEntityActionBase<MetaKind> { // TODO: make base type for item and detail models #localize = new UmbLocalizationController(this); override async execute() { if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.'); - const itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>( + const item = await this.#requestItem(); + + await this._confirmDelete(item); + + const detailRepository = await createExtensionApiByAlias<UmbDetailRepository<any>>( this, - this.args.meta.itemRepositoryAlias, + this.args.meta.detailRepositoryAlias, ); - const { data } = await itemRepository.requestItems([this.args.unique]); - const item = data?.[0]; - if (!item) throw new Error('Item not found.'); + await detailRepository.delete(this.args.unique); + await this.#notify(); + } + + async _confirmDelete(item: any) { const headline = this.args.meta.confirm?.headline ?? '#actions_delete'; const message = this.args.meta.confirm?.message ?? '#defaultdialogs_confirmdelete'; @@ -33,14 +41,26 @@ export class UmbDeleteEntityAction extends UmbEntityActionBase<MetaEntityActionD color: 'danger', confirmLabel: '#general_delete', }); + } - const detailRepository = await createExtensionApiByAlias<UmbDetailRepository<any>>( + async #requestItem() { + if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.'); + + const itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>( this, - this.args.meta.detailRepositoryAlias, + this.args.meta.itemRepositoryAlias, ); - await detailRepository.delete(this.args.unique); + const { data } = await itemRepository.requestItems([this.args.unique]); + const item = data?.[0]; + if (!item) throw new Error('Item not found.'); + + return item; + } + + async #notify() { const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ unique: this.args.unique, entityType: this.args.entityType, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/types.ts new file mode 100644 index 000000000000..b493efd74dfb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/types.ts @@ -0,0 +1 @@ +export type * from './delete/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts index 5fc6da73803d..35d2e26e19e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts @@ -1,5 +1,6 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +export type * from './common/types.js'; export type * from './default/types.js'; export type * from './entity-action-element.interface.js'; export type * from './entity-action.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/constants.ts index 962fe58d3c34..55b3a7405c7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/constants.ts @@ -1 +1,2 @@ export * from './restore-from-recycle-bin/constants.js'; +export * from './trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/constants.ts new file mode 100644 index 000000000000..45dc319a2e66 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/constants.ts @@ -0,0 +1 @@ +export { UMB_ENTITY_ACTION_TRASH_KIND_MANIFEST } from './trash.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.kind.ts index a5b8770a452d..f9155f4fbab1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.kind.ts @@ -1,7 +1,7 @@ import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '../../../entity-action/default/default.action.kind.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: UmbExtensionManifestKind = { +export const UMB_ENTITY_ACTION_TRASH_KIND_MANIFEST: UmbExtensionManifestKind = { type: 'kind', alias: 'Umb.Kind.EntityAction.Trash', matchKind: 'trash', @@ -12,13 +12,12 @@ export const manifest: UmbExtensionManifestKind = { kind: 'trash', api: () => import('./trash.action.js'), weight: 1150, - forEntityTypes: [], meta: { icon: 'icon-trash', label: '#actions_trash', - itemRepositoryAlias: '', - recycleBinRepositoryAlias: '', additionalOptions: true, }, }, }; + +export const manifest = UMB_ENTITY_ACTION_TRASH_KIND_MANIFEST; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.ts index 6896d26586cc..bd83e0976d42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/trash/trash.action.ts @@ -1,19 +1,23 @@ -import { UmbEntityActionBase } from '../../../entity-action/entity-action-base.js'; -import { UmbRequestReloadStructureForEntityEvent } from '../../../entity-action/request-reload-structure-for-entity.event.js'; import type { UmbRecycleBinRepository } from '../../recycle-bin-repository.interface.js'; import type { MetaEntityActionTrashKind } from './types.js'; import { UmbEntityTrashedEvent } from './trash.event.js'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; -import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; /** * Entity action for trashing an item. * @class UmbTrashEntityAction * @augments {UmbEntityActionBase<MetaEntityActionTrashKind>} */ -export class UmbTrashEntityAction extends UmbEntityActionBase<MetaEntityActionTrashKind> { +export class UmbTrashEntityAction< + MetaKindType extends MetaEntityActionTrashKind = MetaEntityActionTrashKind, +> extends UmbEntityActionBase<MetaKindType> { + #localize = new UmbLocalizationController(this); + /** * Executes the action. * @memberof UmbTrashEntityAction @@ -21,30 +25,45 @@ export class UmbTrashEntityAction extends UmbEntityActionBase<MetaEntityActionTr override async execute() { if (!this.args.unique) throw new Error('Cannot trash an item without a unique identifier.'); - const itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>( + const item = await this.#requestItem(); + + await this._confirmTrash(item); + + const recycleBinRepository = await createExtensionApiByAlias<UmbRecycleBinRepository>( this, - this.args.meta.itemRepositoryAlias, + this.args.meta.recycleBinRepositoryAlias, ); - const { data } = await itemRepository.requestItems([this.args.unique]); - const item = data?.[0]; - if (!item) throw new Error('Item not found.'); + await recycleBinRepository.requestTrash({ unique: this.args.unique }); + + this.#notify(); + } + + protected async _confirmTrash(item: any) { + const headline = '#actions_trash'; + const message = '#defaultdialogs_confirmTrash'; // TODO: handle items with variants await umbConfirmModal(this._host, { - headline: `Trash`, - content: `Are you sure you want to move ${item.name} to the recycle bin?`, + headline, + content: this.#localize.string(message, item.name), color: 'danger', - confirmLabel: 'Trash', + confirmLabel: '#actions_trash', }); + } - const recycleBinRepository = await createExtensionApiByAlias<UmbRecycleBinRepository>( + async #requestItem() { + if (!this.args.unique) throw new Error('Cannot trash an item without a unique identifier.'); + + const itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>( this, - this.args.meta.recycleBinRepositoryAlias, + this.args.meta.itemRepositoryAlias, ); - await recycleBinRepository.requestTrash({ unique: this.args.unique }); - this.#notify(); + const { data } = await itemRepository.requestItems([this.args.unique]); + const item = data?.[0]; + if (!item) throw new Error('Item not found.'); + return item; } async #notify() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts index 3c1a1fcd1be2..ecbb1a7e5c24 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts @@ -2,6 +2,7 @@ import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../item/constants.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../user-permissions/constants.js'; +import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from '../reference/constants.js'; import { manifests as createBlueprintManifests } from './create-blueprint/manifests.js'; import { manifests as createManifests } from './create/manifests.js'; import { manifests as cultureAndHostnamesManifests } from './culture-and-hostnames/manifests.js'; @@ -15,13 +16,14 @@ import { UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/r const entityActions: Array<UmbExtensionManifest> = [ { type: 'entityAction', - kind: 'delete', + kind: 'deleteWithRelation', alias: 'Umb.EntityAction.Document.Delete', name: 'Delete Document Entity Action', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, detailRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index bf4f63f5d34a..db98e6e0bbef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -5,6 +5,7 @@ import { UMB_USER_PERMISSION_DOCUMENT_DELETE, UMB_USER_PERMISSION_DOCUMENT_MOVE, } from '../../constants.js'; +import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../item/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, @@ -14,13 +15,14 @@ import { export const manifests: Array<UmbExtensionManifest> = [ { type: 'entityAction', - kind: 'trash', + kind: 'trashWithRelation', alias: 'Umb.EntityAction.Document.RecycleBin.Trash', name: 'Trash Document Entity Action', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, recycleBinRepositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/components/document-reference-table.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/components/document-reference-table.element.ts index 805d7c357c65..5e73b4c2e9a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/components/document-reference-table.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/components/document-reference-table.element.ts @@ -44,7 +44,8 @@ export class UmbDocumentReferenceTableElement extends UmbLitElement { } if (!data) return; - + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore this._items = data.items; this._hasMoreReferences = data.total > this.#pageSize ? data.total - this.#pageSize : 0; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts index 628592c62c18..fa3b1f6dbe3a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts @@ -1,8 +1,9 @@ import { UmbDocumentReferenceServerDataSource } from './document-reference.server.data.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityReferenceRepository } from '@umbraco-cms/backoffice/relations'; -export class UmbDocumentReferenceRepository extends UmbControllerBase { +export class UmbDocumentReferenceRepository extends UmbControllerBase implements UmbEntityReferenceRepository { #referenceSource: UmbDocumentReferenceServerDataSource; constructor(host: UmbControllerHost) { @@ -14,6 +15,11 @@ export class UmbDocumentReferenceRepository extends UmbControllerBase { if (!unique) throw new Error(`unique is required`); return this.#referenceSource.getReferencedBy(unique, skip, take); } + + async requestDescendantsWithReferences(unique: string, skip = 0, take = 20) { + if (!unique) throw new Error(`unique is required`); + return this.#referenceSource.getReferencedDescendants(unique, skip, take); + } } export default UmbDocumentReferenceRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts index 077394524034..dd19c40ddf65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts @@ -1,22 +1,32 @@ +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbEntityReferenceDataSource, UmbReferenceItemModel } from '@umbraco-cms/backoffice/relations'; +import type { UmbPagedModel, UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; import { UmbManagementApiDataMapper } from '@umbraco-cms/backoffice/repository'; /** * @class UmbDocumentReferenceServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentReferenceServerDataSource extends UmbControllerBase { +export class UmbDocumentReferenceServerDataSource extends UmbControllerBase implements UmbEntityReferenceDataSource { #dataMapper = new UmbManagementApiDataMapper(this); /** * Fetches the item for the given unique from the server * @param {string} unique - The unique identifier of the item to fetch - * @returns {*} + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise<UmbDataSourceResponse<UmbPagedModel<UmbReferenceItemModel>>>} - Items that are referenced by the given unique * @memberof UmbDocumentReferenceServerDataSource */ - async getReferencedBy(unique: string, skip = 0, take = 20) { + async getReferencedBy( + unique: string, + skip = 0, + take = 20, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbReferenceItemModel>>> { const { data, error } = await tryExecuteAndNotify( this, DocumentService.getDocumentByIdReferencedBy({ id: unique, skip, take }), @@ -44,4 +54,36 @@ export class UmbDocumentReferenceServerDataSource extends UmbControllerBase { return { data, error }; } + + /** + * Returns any descendants of the given unique that is referenced by other items + * @param {string} unique - The unique identifier of the item to fetch descendants for + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise<UmbDataSourceResponse<UmbPagedModel<UmbEntityModel>>>} - Any descendants of the given unique that is referenced by other items + * @memberof UmbDocumentReferenceServerDataSource + */ + async getReferencedDescendants( + unique: string, + skip: number = 0, + take: number = 20, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbEntityModel>>> { + const { data, error } = await tryExecuteAndNotify( + this, + DocumentService.getDocumentByIdReferencedDescendants({ id: unique, skip, take }), + ); + + if (data) { + const items: Array<UmbEntityModel> = data.items.map((item) => { + return { + unique: item.id, + entityType: UMB_DOCUMENT_ENTITY_TYPE, + }; + }); + + return { data: { items, total: data.total } }; + } + + return { data, error }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/manifests.ts index c27a0c6bb13c..f1c4effd4088 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/manifests.ts @@ -1,4 +1,8 @@ -import { UMB_MEDIA_DETAIL_REPOSITORY_ALIAS, UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../constants.js'; +import { + UMB_MEDIA_DETAIL_REPOSITORY_ALIAS, + UMB_MEDIA_ITEM_REPOSITORY_ALIAS, + UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS, +} from '../constants.js'; import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import { manifests as createManifests } from './create/manifests.js'; import { manifests as moveManifests } from './move-to/manifests.js'; @@ -11,11 +15,12 @@ export const manifests: Array<UmbExtensionManifest> = [ type: 'entityAction', alias: 'Umb.EntityAction.Media.Delete', name: 'Delete Media Entity Action ', - kind: 'delete', + kind: 'deleteWithRelation', forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_MEDIA_ITEM_REPOSITORY_ALIAS, detailRepositoryAlias: UMB_MEDIA_DETAIL_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/item/manifests.ts index ceb7c57fb02c..fe40fdf4416d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/item/manifests.ts @@ -4,7 +4,7 @@ export const manifests: Array<UmbExtensionManifest> = [ { type: 'entityItemRef', alias: 'Umb.EntityItemRef.Media', - name: 'Member Entity Item Reference', + name: 'Media Entity Item Reference', element: () => import('./media-item-ref.element.js'), forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts index b78ea50e0fb7..cf87461c8924 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts @@ -4,6 +4,7 @@ import { UMB_MEDIA_ENTITY_TYPE, } from '../../constants.js'; import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS } from '../constants.js'; +import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, @@ -12,13 +13,14 @@ import { export const manifests: Array<UmbExtensionManifest> = [ { type: 'entityAction', - kind: 'trash', + kind: 'trashWithRelation', alias: 'Umb.EntityAction.Media.RecycleBin.Trash', name: 'Trash Media Entity Action', forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_MEDIA_ITEM_REPOSITORY_ALIAS, recycleBinRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts index dd7bb49c50de..ab5ac83a5fc7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts @@ -1,8 +1,9 @@ import { UmbMediaReferenceServerDataSource } from './media-reference.server.data.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityReferenceRepository } from '@umbraco-cms/backoffice/relations'; -export class UmbMediaReferenceRepository extends UmbControllerBase { +export class UmbMediaReferenceRepository extends UmbControllerBase implements UmbEntityReferenceRepository { #referenceSource: UmbMediaReferenceServerDataSource; constructor(host: UmbControllerHost) { @@ -14,6 +15,11 @@ export class UmbMediaReferenceRepository extends UmbControllerBase { if (!unique) throw new Error(`unique is required`); return this.#referenceSource.getReferencedBy(unique, skip, take); } + + async requestDescendantsWithReferences(unique: string, skip = 0, take = 20) { + if (!unique) throw new Error(`unique is required`); + return this.#referenceSource.getReferencedDescendants(unique, skip, take); + } } export default UmbMediaReferenceRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts index 5c5eac9ea5bf..c3ea9207534d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts @@ -1,22 +1,32 @@ -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbManagementApiDataMapper } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbEntityReferenceDataSource, UmbReferenceItemModel } from '@umbraco-cms/backoffice/relations'; +import type { UmbPagedModel, UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; /** * @class UmbMediaReferenceServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbMediaReferenceServerDataSource extends UmbControllerBase { +export class UmbMediaReferenceServerDataSource extends UmbControllerBase implements UmbEntityReferenceDataSource { #dataMapper = new UmbManagementApiDataMapper(this); /** - * Fetches the item for the given id from the server + * Fetches the item for the given unique from the server * @param {string} unique - The unique identifier of the item to fetch - * @returns {*} + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise<UmbDataSourceResponse<UmbPagedModel<UmbReferenceItemModel>>>} - Items that are referenced by the given unique * @memberof UmbMediaReferenceServerDataSource */ - async getReferencedBy(unique: string, skip = 0, take = 20) { + async getReferencedBy( + unique: string, + skip: number = 0, + take: number = 20, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbReferenceItemModel>>> { const { data, error } = await tryExecuteAndNotify( this, MediaService.getMediaByIdReferencedBy({ id: unique, skip, take }), @@ -44,4 +54,36 @@ export class UmbMediaReferenceServerDataSource extends UmbControllerBase { return { data, error }; } + + /** + * Returns any descendants of the given unique that is referenced by other items + * @param {string} unique - The unique identifier of the item to fetch descendants for + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise<UmbDataSourceResponse<UmbPagedModel<UmbEntityModel>>>} - Any descendants of the given unique that is referenced by other items + * @memberof UmbMediaReferenceServerDataSource + */ + async getReferencedDescendants( + unique: string, + skip: number = 0, + take: number = 20, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbEntityModel>>> { + const { data, error } = await tryExecuteAndNotify( + this, + MediaService.getMediaByIdReferencedDescendants({ id: unique, skip, take }), + ); + + if (data) { + const items: Array<UmbEntityModel> = data.items.map((item) => { + return { + unique: item.id, + entityType: UMB_MEDIA_ENTITY_TYPE, + }; + }); + + return { data: { items, total: data.total } }; + } + + return { data, error }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/manifests.ts index 66c4ca9ee9ab..0f5c2ccc4c99 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/manifests.ts @@ -2,8 +2,9 @@ import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as relationManifests } from './relations/manifests.js'; import { manifests as relationTypeManifests } from './relation-types/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array<UmbExtensionManifest> = [ +export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [ ...menuManifests, ...relationManifests, ...relationTypeManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts index fcb47bcacf62..1ad52da7ec8a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts @@ -1,2 +1,4 @@ -export * from './collection/constants.js'; export { UMB_RELATION_ENTITY_TYPE } from './entity.js'; +export * from './collection/constants.js'; +export * from './entity-actions/delete/constants.js'; +export * from './entity-actions/trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/constants.ts new file mode 100644 index 000000000000..26f4f0dd5f39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/constants.ts @@ -0,0 +1 @@ +export * from './modal/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.kind.ts new file mode 100644 index 000000000000..cc1b0a57cc9e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.kind.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_ENTITY_ACTION_DELETE_KIND_MANIFEST } from '@umbraco-cms/backoffice/entity-action'; + +export const manifest: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityAction.DeleteWithRelation', + matchKind: 'deleteWithRelation', + matchType: 'entityAction', + manifest: { + ...UMB_ENTITY_ACTION_DELETE_KIND_MANIFEST.manifest, + type: 'entityAction', + kind: 'deleteWithRelation', + api: () => import('./delete-with-relation.action.js'), + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.ts new file mode 100644 index 000000000000..89e5c23c7e1d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/delete-with-relation.action.ts @@ -0,0 +1,30 @@ +import type { MetaEntityActionDeleteWithRelationKind } from './types.js'; +import { UMB_DELETE_WITH_RELATION_CONFIRM_MODAL } from './modal/constants.js'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbDeleteEntityAction } from '@umbraco-cms/backoffice/entity-action'; + +/** + * Entity action for deleting an item with relations. + * @class UmbDeleteWithRelationEntityAction + * @augments {UmbEntityActionBase<MetaEntityActionDeleteWithRelationKind>} + */ +export class UmbDeleteWithRelationEntityAction extends UmbDeleteEntityAction<MetaEntityActionDeleteWithRelationKind> { + override async _confirmDelete() { + if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.'); + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + + const modal = modalManager.open(this, UMB_DELETE_WITH_RELATION_CONFIRM_MODAL, { + data: { + unique: this.args.unique, + entityType: this.args.entityType, + itemRepositoryAlias: this.args.meta.itemRepositoryAlias, + referenceRepositoryAlias: this.args.meta.referenceRepositoryAlias, + }, + }); + + await modal.onSubmit(); + } +} + +export { UmbDeleteWithRelationEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/index.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/index.ts new file mode 100644 index 000000000000..a609dad289f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/index.ts @@ -0,0 +1,2 @@ +export * from './delete-with-relation.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/manifests.ts new file mode 100644 index 000000000000..81aff4a3cf03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/manifests.ts @@ -0,0 +1,9 @@ +import { manifest as deleteKindManifest } from './delete-with-relation.action.kind.js'; +import { manifests as modalManifests } from './modal/manifests.js'; + +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [ + deleteKindManifest, + ...modalManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/constants.ts new file mode 100644 index 000000000000..1081167efe8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/constants.ts @@ -0,0 +1 @@ +export * from './delete-with-relation-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.element.ts new file mode 100644 index 000000000000..539568c271ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.element.ts @@ -0,0 +1,107 @@ +import type { + UmbDeleteWithRelationConfirmModalData, + UmbDeleteWithRelationConfirmModalValue, +} from './delete-with-relation-modal.token.js'; +import { + html, + customElement, + css, + state, + type PropertyValues, + nothing, + unsafeHTML, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; + +import '../../local-components/confirm-action-entity-references.element.js'; + +@customElement('umb-delete-with-relation-confirm-modal') +export class UmbDeleteWithRelationConfirmModalElement extends UmbModalBaseElement< + UmbDeleteWithRelationConfirmModalData, + UmbDeleteWithRelationConfirmModalValue +> { + @state() + _name?: string; + + @state() + _referencesConfig?: any; + + #itemRepository?: UmbItemRepository<any>; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.data) { + this.#itemRepository?.destroy(); + return; + } + + this.#itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>(this, this.data.itemRepositoryAlias); + + const { data } = await this.#itemRepository.requestItems([this.data.unique]); + const item = data?.[0]; + if (!item) throw new Error('Item not found.'); + + this._name = item.name; + + this._referencesConfig = { + unique: this.data.unique, + itemRepositoryAlias: this.data.itemRepositoryAlias, + referenceRepositoryAlias: this.data.referenceRepositoryAlias, + }; + } + + override render() { + const headline = this.localize.string('#actions_delete'); + const content = this.localize.string('#defaultdialogs_confirmdelete', this._name); + + return html` + <uui-dialog-layout class="uui-text" headline=${headline}> + <p>${unsafeHTML(content)}</p> + ${this._referencesConfig + ? html`<umb-confirm-action-modal-entity-references + .config=${this._referencesConfig}></umb-confirm-action-modal-entity-references>` + : nothing} + + <uui-button + slot="actions" + id="cancel" + label=${this.localize.term('general_cancel')} + @click=${this._rejectModal}></uui-button> + + <uui-button + slot="actions" + id="confirm" + color="danger" + look="primary" + label=${this.localize.term('general_delete')} + @click=${this._submitModal} + ${umbFocus()}></uui-button> + </uui-dialog-layout> + `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + `, + ]; +} + +export { UmbDeleteWithRelationConfirmModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-delete-with-relation-confirm-modal': UmbDeleteWithRelationConfirmModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.token.ts new file mode 100644 index 000000000000..c3ba603bb5ba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/delete-with-relation-modal.token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDeleteWithRelationConfirmModalData { + unique: string; + entityType: string; + itemRepositoryAlias: string; + referenceRepositoryAlias: string; +} + +export type UmbDeleteWithRelationConfirmModalValue = undefined; + +export const UMB_DELETE_WITH_RELATION_CONFIRM_MODAL = new UmbModalToken< + UmbDeleteWithRelationConfirmModalData, + UmbDeleteWithRelationConfirmModalValue +>('Umb.Modal.DeleteWithRelation', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/manifests.ts new file mode 100644 index 000000000000..8689b6d7837c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array<UmbExtensionManifest> = [ + { + type: 'modal', + alias: 'Umb.Modal.DeleteWithRelation', + name: 'Delete With Relation Modal', + element: () => import('./delete-with-relation-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/types.ts new file mode 100644 index 000000000000..d53ef50d1fcc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/delete/types.ts @@ -0,0 +1,17 @@ +import type { ManifestEntityAction, MetaEntityActionDeleteKind } from '@umbraco-cms/backoffice/entity-action'; + +export interface ManifestEntityActionDeleteWithRelationKind + extends ManifestEntityAction<MetaEntityActionDeleteWithRelationKind> { + type: 'entityAction'; + kind: 'deleteWithRelation'; +} + +export interface MetaEntityActionDeleteWithRelationKind extends MetaEntityActionDeleteKind { + referenceRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityActionDeleteWithRelationKind: ManifestEntityActionDeleteWithRelationKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-action-entity-references.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-action-entity-references.element.ts new file mode 100644 index 000000000000..73c8d8b41abe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-action-entity-references.element.ts @@ -0,0 +1,172 @@ +import type { UmbEntityReferenceRepository, UmbReferenceItemModel } from '../../reference/types.js'; +import { + html, + customElement, + css, + state, + nothing, + type PropertyValues, + property, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-confirm-action-modal-entity-references') +export class UmbConfirmActionModalEntityReferencesElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + config?: { + itemRepositoryAlias: string; + referenceRepositoryAlias: string; + entityType: string; + unique: string; + }; + + @state() + _referencedByItems: Array<UmbReferenceItemModel> = []; + + @state() + _totalReferencedByItems: number = 0; + + @state() + _totalDescendantsWithReferences: number = 0; + + @state() + _descendantsWithReferences: Array<any> = []; + + #itemRepository?: UmbItemRepository<any>; + #referenceRepository?: UmbEntityReferenceRepository; + + #limitItems = 3; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.config) { + this.#itemRepository?.destroy(); + this.#referenceRepository?.destroy(); + return; + } + + if (!this.config?.referenceRepositoryAlias) { + throw new Error('Missing referenceRepositoryAlias in config.'); + } + + this.#referenceRepository = await createExtensionApiByAlias<UmbEntityReferenceRepository>( + this, + this.config?.referenceRepositoryAlias, + ); + + if (!this.config?.itemRepositoryAlias) { + throw new Error('Missing itemRepositoryAlias in config.'); + } + + this.#itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>( + this, + this.config.itemRepositoryAlias, + ); + + this.#loadReferencedBy(); + this.#loadDescendantsWithReferences(); + } + + async #loadReferencedBy() { + if (!this.#referenceRepository) { + throw new Error('Failed to create reference repository.'); + } + + if (!this.config?.unique) { + throw new Error('Missing unique in data.'); + } + + const { data } = await this.#referenceRepository.requestReferencedBy(this.config.unique, 0, this.#limitItems); + + if (data) { + this._referencedByItems = [...data.items]; + this._totalReferencedByItems = data.total; + } + } + + async #loadDescendantsWithReferences() { + if (!this.#referenceRepository) { + throw new Error('Failed to create reference repository.'); + } + + if (!this.#itemRepository) { + throw new Error('Failed to create item repository.'); + } + + // If the repository does not have the method, we don't need to load the referenced descendants. + if (!this.#referenceRepository.requestDescendantsWithReferences) return; + + if (!this.config?.unique) { + throw new Error('Missing unique in data.'); + } + + const { data } = await this.#referenceRepository.requestDescendantsWithReferences( + this.config.unique, + 0, + this.#limitItems, + ); + + if (data) { + this._totalDescendantsWithReferences = data.total; + const uniques = data.items.map((item) => item.unique).filter((unique) => unique) as Array<string>; + const { data: items } = await this.#itemRepository.requestItems(uniques); + this._descendantsWithReferences = items ?? []; + } + } + + override render() { + return html` + ${this.#renderItems('references_labelDependsOnThis', this._referencedByItems, this._totalReferencedByItems)} + ${this.#renderItems( + 'references_labelDependentDescendants', + this._descendantsWithReferences, + this._totalDescendantsWithReferences, + )} + `; + } + + #renderItems(headline: string, items: Array<UmbReferenceItemModel>, total: number) { + if (total === 0) return nothing; + + return html` + <h5 id="reference-headline">${this.localize.term(headline)}</h5> + <uui-ref-list> + ${items.map( + (item) => + html`<umb-entity-item-ref .item=${item} readonly ?standalone=${total === 1}></umb-entity-item-ref> `, + )} + </uui-ref-list> + ${total > this.#limitItems + ? html`<span>${this.localize.term('references_labelMoreReferences', total - this.#limitItems)}</span>` + : nothing} + `; + } + + static override styles = [ + UmbTextStyles, + css` + #reference-headline { + margin-bottom: var(--uui-size-3); + } + + uui-ref-list { + margin-bottom: var(--uui-size-2); + } + `, + ]; +} + +export { UmbConfirmActionModalEntityReferencesElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-confirm-action-modal-entity-references': UmbConfirmActionModalEntityReferencesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/constants.ts new file mode 100644 index 000000000000..26f4f0dd5f39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/constants.ts @@ -0,0 +1 @@ +export * from './modal/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/index.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/index.ts new file mode 100644 index 000000000000..cca1fbe202bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/index.ts @@ -0,0 +1,2 @@ +export * from './trash-with-relation.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/manifests.ts new file mode 100644 index 000000000000..607b77322dee --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/manifests.ts @@ -0,0 +1,6 @@ +import { manifest as trashKindManifest } from './trash-with-relation.action.kind.js'; +import { manifests as modalManifests } from './modal/manifests.js'; + +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [trashKindManifest, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/constants.ts new file mode 100644 index 000000000000..4f885f7678f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/constants.ts @@ -0,0 +1 @@ +export * from './trash-with-relation-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/manifests.ts new file mode 100644 index 000000000000..4b310dd31ef5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array<UmbExtensionManifest> = [ + { + type: 'modal', + alias: 'Umb.Modal.TrashWithRelation', + name: 'Trash With Relation Modal', + element: () => import('./trash-with-relation-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.element.ts new file mode 100644 index 000000000000..61f40cf2bcb2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.element.ts @@ -0,0 +1,105 @@ +import type { + UmbTrashWithRelationConfirmModalData, + UmbTrashWithRelationConfirmModalValue, +} from './trash-with-relation-modal.token.js'; +import { + html, + customElement, + css, + state, + type PropertyValues, + nothing, + unsafeHTML, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; + +// import of local component +import '../../local-components/confirm-action-entity-references.element.js'; + +@customElement('umb-trash-with-relation-confirm-modal') +export class UmbTrashWithRelationConfirmModalElement extends UmbModalBaseElement< + UmbTrashWithRelationConfirmModalData, + UmbTrashWithRelationConfirmModalValue +> { + @state() + _name?: string; + + @state() + _referencesConfig?: any; + + #itemRepository?: UmbItemRepository<any>; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.data) { + this.#itemRepository?.destroy(); + return; + } + + this.#itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>(this, this.data.itemRepositoryAlias); + + const { data } = await this.#itemRepository.requestItems([this.data.unique]); + const item = data?.[0]; + if (!item) throw new Error('Item not found.'); + + this._name = item.name; + + this._referencesConfig = { + unique: this.data.unique, + itemRepositoryAlias: this.data.itemRepositoryAlias, + referenceRepositoryAlias: this.data.referenceRepositoryAlias, + }; + } + + override render() { + const headline = this.localize.string('#actions_trash'); + const content = this.localize.string('#defaultdialogs_confirmTrash', this._name); + + return html` + <uui-dialog-layout class="uui-text" headline=${headline}> + <p>${unsafeHTML(content)}</p> + + ${this._referencesConfig + ? html`<umb-confirm-action-modal-entity-references + .config=${this._referencesConfig}></umb-confirm-action-modal-entity-references>` + : nothing} + + <uui-button slot="actions" id="cancel" label="Cancel" @click=${this._rejectModal}></uui-button> + + <uui-button + slot="actions" + id="confirm" + color="danger" + look="primary" + label=${this.localize.term('actions_trash')} + @click=${this._submitModal} + ${umbFocus()}></uui-button> + </uui-dialog-layout> + `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + `, + ]; +} + +export { UmbTrashWithRelationConfirmModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-trash-with-relation-confirm-modal': UmbTrashWithRelationConfirmModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.token.ts new file mode 100644 index 000000000000..6e67d47f1a3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/modal/trash-with-relation-modal.token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbTrashWithRelationConfirmModalData { + unique: string; + entityType: string; + itemRepositoryAlias: string; + referenceRepositoryAlias: string; +} + +export type UmbTrashWithRelationConfirmModalValue = undefined; + +export const UMB_TRASH_WITH_RELATION_CONFIRM_MODAL = new UmbModalToken< + UmbTrashWithRelationConfirmModalData, + UmbTrashWithRelationConfirmModalValue +>('Umb.Modal.TrashWithRelation', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.kind.ts new file mode 100644 index 000000000000..e2ceee56ad09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.kind.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_ENTITY_ACTION_TRASH_KIND_MANIFEST } from '@umbraco-cms/backoffice/recycle-bin'; + +export const manifest: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityAction.TrashWithRelation', + matchKind: 'trashWithRelation', + matchType: 'entityAction', + manifest: { + ...UMB_ENTITY_ACTION_TRASH_KIND_MANIFEST.manifest, + type: 'entityAction', + kind: 'trashWithRelation', + api: () => import('./trash-with-relation.action.js'), + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.ts new file mode 100644 index 000000000000..f5ac8dd4518c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/trash-with-relation.action.ts @@ -0,0 +1,28 @@ +import type { MetaEntityActionTrashWithRelationKind } from './types.js'; +import { UMB_TRASH_WITH_RELATION_CONFIRM_MODAL } from './modal/constants.js'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbTrashEntityAction } from '@umbraco-cms/backoffice/recycle-bin'; + +/** + * Entity action for trashing an item with relations. + * @class UmbTrashWithRelationEntityAction + * @augments {UmbEntityActionBase<MetaEntityActionTrashWithRelationKind>} + */ +export class UmbTrashWithRelationEntityAction extends UmbTrashEntityAction<MetaEntityActionTrashWithRelationKind> { + override async _confirmTrash(item: any) { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + + const modal = modalManager.open(this, UMB_TRASH_WITH_RELATION_CONFIRM_MODAL, { + data: { + unique: item.unique, + entityType: item.entityType, + itemRepositoryAlias: this.args.meta.itemRepositoryAlias, + referenceRepositoryAlias: this.args.meta.referenceRepositoryAlias, + }, + }); + + await modal.onSubmit(); + } +} + +export { UmbTrashWithRelationEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/types.ts new file mode 100644 index 000000000000..a02b000e8bcd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/trash/types.ts @@ -0,0 +1,18 @@ +import type { ManifestEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import type { MetaEntityActionTrashKind } from '@umbraco-cms/backoffice/recycle-bin'; + +export interface ManifestEntityActionTrashWithRelationKind + extends ManifestEntityAction<MetaEntityActionTrashWithRelationKind> { + type: 'entityAction'; + kind: 'trashWithRelation'; +} + +export interface MetaEntityActionTrashWithRelationKind extends MetaEntityActionTrashKind { + referenceRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityActionTrashWithRelationKind: ManifestEntityActionTrashWithRelationKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts index bfab902d3c4e..574ed691cfbd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts @@ -1,3 +1,10 @@ import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as deleteManifests } from './entity-actions/delete/manifests.js'; +import { manifests as trashManifests } from './entity-actions/trash/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array<UmbExtensionManifest> = [...collectionManifests]; +export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [ + ...collectionManifests, + ...deleteManifests, + ...trashManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts new file mode 100644 index 000000000000..a84034a6421b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts @@ -0,0 +1,42 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { + DefaultReferenceResponseModel, + DocumentReferenceResponseModel, + MediaReferenceResponseModel, +} from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbDataSourceResponse, UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbReferenceItemModel extends UmbEntityModel {} + +export type UmbReferenceModel = + | DefaultReferenceResponseModel + | DocumentReferenceResponseModel + | MediaReferenceResponseModel; + +export interface UmbEntityReferenceRepository extends UmbApi { + requestReferencedBy( + unique: string, + skip?: number, + take?: number, + ): Promise<UmbRepositoryResponse<UmbPagedModel<UmbReferenceItemModel>>>; + requestDescendantsWithReferences?( + unique: string, + skip?: number, + take?: number, + ): Promise<UmbRepositoryResponse<UmbPagedModel<UmbEntityModel>>>; +} + +export interface UmbEntityReferenceDataSource { + getReferencedBy( + unique: string, + skip?: number, + take?: number, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbReferenceItemModel>>>; + getReferencedDescendants?( + unique: string, + skip?: number, + take?: number, + ): Promise<UmbDataSourceResponse<UmbPagedModel<UmbEntityModel>>>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/types.ts index cf7867b4dc33..f82c4baafe95 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/types.ts @@ -1,10 +1,5 @@ import type { UmbRelationEntityType } from './entity.js'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -import type { - DefaultReferenceResponseModel, - DocumentReferenceResponseModel, - MediaReferenceResponseModel, -} from '@umbraco-cms/backoffice/external/backend-api'; +export type * from './reference/types.js'; export interface UmbRelationDetailModel { unique: string; @@ -23,11 +18,3 @@ export interface UmbRelationDetailModel { createDate: string; comment: string | null; } - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface UmbReferenceItemModel extends UmbEntityModel {} - -export type UmbReferenceModel = - | DefaultReferenceResponseModel - | DocumentReferenceResponseModel - | MediaReferenceResponseModel;