diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts index 33704df22f..5730bfbf75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts @@ -1,4 +1,5 @@ export * from './publish-modal/index.js'; +export * from './publish-with-descendants-modal/index.js'; export * from './save-modal/index.js'; export * from './unpublish-modal/index.js'; export * from './schedule-modal/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/manifests.ts index 55b1646df4..0951f2da46 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/manifests.ts @@ -4,6 +4,7 @@ export const UMB_DOCUMENT_SAVE_MODAL_ALIAS = 'Umb.Modal.DocumentSave'; export const UMB_DOCUMENT_PUBLISH_MODAL_ALIAS = 'Umb.Modal.DocumentPublish'; export const UMB_DOCUMENT_UNPUBLISH_MODAL_ALIAS = 'Umb.Modal.DocumentUnpublish'; export const UMB_DOCUMENT_SCHEDULE_MODAL_ALIAS = 'Umb.Modal.DocumentSchedule'; +export const UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS = 'Umb.Modal.DocumentPublishWithDescendants'; const modals: Array = [ { @@ -30,6 +31,12 @@ const modals: Array = [ name: 'Document Schedule Modal', js: () => import('./schedule-modal/document-schedule-modal.element.js'), }, + { + type: 'modal', + alias: UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS, + name: 'Document Publish With Descendants Modal', + js: () => import('./publish-with-descendants-modal/document-publish-with-descendants-modal.element.js'), + }, ]; export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.element.ts new file mode 100644 index 0000000000..23a38bf906 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.element.ts @@ -0,0 +1,124 @@ +import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js'; +import type { + UmbDocumentPublishWithDescendantsModalData, + UmbDocumentPublishWithDescendantsModalValue, +} from './document-publish-with-descendants-modal.token.js'; +import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; + +import '../shared/document-variant-language-picker.element.js'; + +@customElement('umb-document-publish-with-descendants-modal') +export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseElement< + UmbDocumentPublishWithDescendantsModalData, + UmbDocumentPublishWithDescendantsModalValue +> { + #selectionManager = new UmbSelectionManager(this); + #includeUnpublishedDescendants = false; + + @state() + _options: Array = []; + + firstUpdated() { + this.#configureSelectionManager(); + } + + async #configureSelectionManager() { + this.#selectionManager.setMultiple(true); + this.#selectionManager.setSelectable(true); + + // Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes: + this._options = + this.data?.options.filter( + (option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED, + ) ?? []; + + let selected = this.value?.selection ?? []; + + // Filter selection based on options: + selected = selected.filter((s) => this._options.some((o) => o.unique === s)); + + this.#selectionManager.setSelection(selected); + + // Additionally select mandatory languages: + this._options.forEach((variant) => { + if (variant.language?.isMandatory) { + this.#selectionManager.select(variant.unique); + } + }); + } + + #submit() { + this.value = { + selection: this.#selectionManager.getSelection(), + includeUnpublishedDescendants: this.#includeUnpublishedDescendants, + }; + this.modalContext?.submit(); + } + + #close() { + this.modalContext?.reject(); + } + + render() { + return html` +

+ ${this._options.length === 1 + ? html` + Publish ${this._options[0].variant?.name} and all content items underneath and thereby + making their content publicly available. + ` + : html` + + Publish variants and variants of same type underneath and thereby making their content publicly + available. + + `} +

+ + + + + (this.#includeUnpublishedDescendants = !this.#includeUnpublishedDescendants)}> + + +
+ + +
+
`; + } + + static styles = [ + UmbTextStyles, + css` + :host { + display: block; + width: 400px; + max-width: 90vw; + } + `, + ]; +} + +export default UmbDocumentPublishWithDescendantsModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-publish-with-descendants-modal': UmbDocumentPublishWithDescendantsModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.stories.ts new file mode 100644 index 0000000000..bfb78a0ff6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.stories.ts @@ -0,0 +1,176 @@ +import './document-publish-with-descendants-modal.element.js'; + +import type { Meta, StoryObj } from '@storybook/web-components'; +import { UmbDocumentVariantState } from '../../types.js'; +import type { + UmbDocumentPublishWithDescendantsModalData, + UmbDocumentPublishWithDescendantsModalValue, +} from './document-publish-with-descendants-modal.token.js'; +import type { UmbDocumentPublishWithDescendantsModalElement } from './document-publish-with-descendants-modal.element.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +const modalData: UmbDocumentPublishWithDescendantsModalData = { + options: [ + { + unique: 'en-us', + culture: 'en-us', + segment: null, + variant: { + name: 'English variant name', + culture: 'en-us', + state: UmbDocumentVariantState.PUBLISHED, + createDate: '2021-08-25T14:00:00Z', + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'English', + unique: 'en-us', + isDefault: true, + isMandatory: true, + fallbackIsoCode: null, + }, + }, + { + unique: 'en-gb', + culture: 'en-gb', + segment: null, + variant: { + name: 'English (GB)', + culture: 'en-us', + segment: null, + state: UmbDocumentVariantState.DRAFT, + createDate: '2021-08-25T14:00:00Z', + publishDate: null, + updateDate: null, + }, + language: { + entityType: 'language', + name: 'English (GB)', + unique: 'en-gb', + isDefault: true, + isMandatory: false, + fallbackIsoCode: null, + }, + }, + { + unique: 'da-dk', + culture: 'da-dk', + segment: null, + variant: { + name: 'Danish variant name', + culture: 'da-dk', + state: UmbDocumentVariantState.NOT_CREATED, + createDate: null, + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'Danish', + unique: 'da-dk', + isDefault: false, + isMandatory: false, + fallbackIsoCode: null, + }, + }, + ], +}; + +const modalValue: UmbDocumentPublishWithDescendantsModalValue = { + selection: ['en-us'], +}; + +const meta: Meta = { + title: 'Workspaces/Document/Modals/Publish With Descendants Modal', + component: 'umb-document-publish-with-descendants-modal', + id: 'umb-document-publish-with-descendants-modal', + args: { + data: modalData, + value: modalValue, + }, + decorators: [(Story) => html`
${Story()}
`], + parameters: { + layout: 'centered', + docs: { + source: { + language: 'ts', + code: ` +import { UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL, UmbDocumentVariantState } from '@umbraco-cms/backoffice/document'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => { + const result = modalManager.open(this, UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL, { + data: { + options: [ + { + unique: 'en-us', + culture: 'en-us', + segment: null, + variant: { + name: 'English variant name', + culture: 'en-us', + state: UmbDocumentVariantState.PUBLISHED, + createDate: '2021-08-25T14:00:00Z', + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'English', + unique: 'en-us', + isDefault: true, + isMandatory: true, + fallbackIsoCode: null, + }, + }, + { + unique: 'da-dk', + culture: 'da-dk', + segment: null, + variant: { + name: 'Danish variant name', + culture: 'da-dk', + state: UmbDocumentVariantState.NOT_CREATED, + createDate: null, + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'Danish', + unique: 'da-dk', + isDefault: false, + isMandatory: false, + fallbackIsoCode: null, + }, + }, + ], + } + }).onSubmit().catch(() => undefined); +}); + `, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = {}; + +export const Invariant: Story = { + args: { + data: { + ...modalData, + options: modalData.options.slice(0, 1), + }, + value: modalValue, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.token.ts new file mode 100644 index 0000000000..84c5a9b9aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.token.ts @@ -0,0 +1,18 @@ +import type { UmbDocumentVariantPickerData, UmbDocumentVariantPickerValue } from '../types.js'; +import { UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS } from '../manifests.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDocumentPublishWithDescendantsModalData extends UmbDocumentVariantPickerData {} + +export interface UmbDocumentPublishWithDescendantsModalValue extends UmbDocumentVariantPickerValue { + includeUnpublishedDescendants?: boolean; +} + +export const UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL = new UmbModalToken< + UmbDocumentPublishWithDescendantsModalData, + UmbDocumentPublishWithDescendantsModalValue +>(UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS, { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/index.ts new file mode 100644 index 0000000000..310db622dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/publish-with-descendants-modal/index.ts @@ -0,0 +1 @@ +export * from './document-publish-with-descendants-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.repository.ts index d72e0d1ead..2c62c85dac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.repository.ts @@ -65,4 +65,27 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { return { error }; } + + /** + * Publish variants of a document including its descendants + * @memberof UmbDocumentPublishingRepository + */ + async publishWithDescendants(id: string, variantIds: Array, includeUnpublishedDescendants: boolean) { + if (!id) throw new Error('id is missing'); + if (!variantIds) throw new Error('variant IDs are missing'); + await this.#init; + + const { error } = await this.#publishingDataSource.publishWithDescendants( + id, + variantIds, + includeUnpublishedDescendants, + ); + + if (!error) { + const notification = { data: { message: `Document published with descendants` } }; + this.#notificationContext?.peek('positive', notification); + } + + return { error }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.server.data-source.ts index eb3efdf3f2..20cc82657a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/publishing/document-publishing.server.data-source.ts @@ -2,6 +2,7 @@ import type { UmbDocumentVariantPublishModel } from '../../types.js'; import type { CultureAndScheduleRequestModel, PublishDocumentRequestModel, + PublishDocumentWithDescendantsRequestModel, UnpublishDocumentRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -32,7 +33,7 @@ export class UmbDocumentPublishingServerDataSource { * @param {string} unique * @param {Array} variantIds * @return {*} - * @memberof UmbDocumentServerDataSource + * @memberof UmbDocumentPublishingServerDataSource */ async publish(unique: string, variants: Array) { if (!unique) throw new Error('Id is missing'); @@ -59,7 +60,7 @@ export class UmbDocumentPublishingServerDataSource { * @param {string} unique * @param {Array} variantIds * @return {*} - * @memberof UmbDocumentServerDataSource + * @memberof UmbDocumentPublishingServerDataSource */ async unpublish(unique: string, variantIds: Array) { if (!unique) throw new Error('Id is missing'); @@ -83,4 +84,26 @@ export class UmbDocumentPublishingServerDataSource { return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentByIdUnpublish({ id: unique, requestBody })); } + + /** + * Publish variants of a document and all its descendants + * @memberof UmbDocumentPublishingServerDataSource + */ + async publishWithDescendants( + unique: string, + variantIds: Array, + includeUnpublishedDescendants: boolean, + ) { + if (!unique) throw new Error('Id is missing'); + + const requestBody: PublishDocumentWithDescendantsRequestModel = { + cultures: variantIds.map((variant) => variant.toCultureString()), + includeUnpublishedDescendants, + }; + + return tryExecuteAndNotify( + this.#host, + DocumentResource.putDocumentByIdPublishWithDescendants({ id: unique, requestBody }), + ); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/publish-with-descendants.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/publish-with-descendants.action.ts new file mode 100644 index 0000000000..f1c1fe9fe3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/publish-with-descendants.action.ts @@ -0,0 +1,9 @@ +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; +import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; + +export class UmbDocumentPublishWithDescendantsWorkspaceAction extends UmbWorkspaceActionBase { + async execute() { + const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + return workspaceContext.publishWithDescendants(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index d72ca6d092..134348bcf6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -9,7 +9,12 @@ import type { UmbDocumentVariantModel, UmbDocumentVariantOptionModel, } from '../types.js'; -import { UMB_DOCUMENT_PUBLISH_MODAL, UMB_DOCUMENT_SCHEDULE_MODAL } from '../modals/index.js'; +import { + UMB_DOCUMENT_PUBLISH_MODAL, + UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL, + UMB_DOCUMENT_SCHEDULE_MODAL, + UMB_DOCUMENT_SAVE_MODAL, +} from '../modals/index.js'; import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js'; import { UmbUnpublishDocumentEntityAction } from '../entity-actions/unpublish.action.js'; import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js'; @@ -36,7 +41,6 @@ import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; -import { UMB_DOCUMENT_SAVE_MODAL } from '../modals/save-modal/document-save-modal.token.js'; type EntityType = UmbDocumentDetailModel; export class UmbDocumentWorkspaceContext @@ -625,6 +629,44 @@ export class UmbDocumentWorkspaceContext new UmbUnpublishDocumentEntityAction(this, { unique, entityType, meta: {} as never }).execute(); } + public async publishWithDescendants() { + const { options, selected } = await this.#determineVariantOptions(); + + const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const result = await modalManagerContext + .open(this, UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL, { + data: { + options, + }, + value: { selection: selected }, + }) + .onSubmit() + .catch(() => undefined); + + if (!result?.selection.length) return; + + // Map to variantIds + const variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; + + if (!variantIds.length) return; + + const unique = this.getUnique(); + if (!unique) throw new Error('Unique is missing'); + await this.publishingRepository.publishWithDescendants( + unique, + variantIds, + result.includeUnpublishedDescendants ?? false, + ); + + const data = this.getData(); + if (!data) throw new Error('Data is missing'); + + this.#persistedData.setValue(data); + this.#currentData.setValue(data); + + this.workspaceComplete(data); + } + async delete() { const id = this.getUnique(); if (id) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts index 6d15985fa6..3734303e67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts @@ -2,6 +2,7 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UmbDocumentSaveAndScheduleWorkspaceAction } from './actions/save-and-schedule.action.js'; import { UmbDocumentUnpublishWorkspaceAction } from './actions/unpublish.action.js'; import { UmbDocumentSaveAndPublishWorkspaceAction } from './actions/save-and-publish.action.js'; +import { UmbDocumentPublishWithDescendantsWorkspaceAction } from './actions/publish-with-descendants.action.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, @@ -148,7 +149,7 @@ const workspaceActionMenuItems: Array = [ kind: 'default', alias: 'Umb.Document.WorkspaceActionMenuItem.Unpublish', name: 'Unpublish', - weight: 10, + weight: 0, api: UmbDocumentUnpublishWorkspaceAction, forWorkspaceActions: 'Umb.WorkspaceAction.Document.SaveAndPublish', meta: { @@ -161,11 +162,11 @@ const workspaceActionMenuItems: Array = [ kind: 'default', alias: 'Umb.Document.WorkspaceActionMenuItem.PublishWithDescendants', name: 'Publish with descendants', - weight: 20, - api: UmbDocumentSaveAndPublishWorkspaceAction, + weight: 10, + api: UmbDocumentPublishWithDescendantsWorkspaceAction, forWorkspaceActions: 'Umb.WorkspaceAction.Document.SaveAndPublish', meta: { - label: 'Publish with descendants (TBD)', + label: 'Publish with descendants...', icon: 'icon-globe', }, },