diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 8b876086c390..22cda2c9b219 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -1412,14 +1412,14 @@ export default { cssSavedText: 'Stylesheet saved without any errors', dataTypeSaved: 'Datatype saved', dictionaryItemSaved: 'Dictionary item saved', - editContentPublishedHeader: 'Content published', + editContentPublishedHeader: 'Document published', editContentPublishedText: 'and is visible on the website', - editMultiContentPublishedText: '%0% documents published and visible on the website', - editVariantPublishedText: '%0% published and visible on the website', - editMultiVariantPublishedText: '%0% documents published for languages %1% and visible on the website', + editMultiContentPublishedText: '%0% documents published and are visible on the website', + editVariantPublishedText: '%0% published and is visible on the website', + editMultiVariantPublishedText: '%0% documents published for languages %1% and are visible on the website', editBlueprintSavedHeader: 'Document Blueprint saved', editBlueprintSavedText: 'Changes have been successfully saved', - editContentSavedHeader: 'Content saved', + editContentSavedHeader: 'Document saved', editContentSavedText: 'Remember to publish to make changes visible', editContentScheduledSavedText: 'A schedule for publishing has been updated', editVariantSavedText: '%0% saved', 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 40c1be0e3f3e..3dd24d3ce59f 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -1412,7 +1412,7 @@ export default { folderUploadNotAllowed: 'This file is being uploaded as part of a folder, but creating a new folder is not allowed here', folderCreationNotAllowed: 'Creating a new folder is not allowed here', - contentPublishedFailedByEvent: 'Content could not be published, a 3rd party add-in cancelled the action', + contentPublishedFailedByEvent: 'Document could not be published, a 3rd party add-in cancelled the action', contentTypeDublicatePropertyType: 'Property type already exists', contentTypePropertyTypeCreated: 'Property type created', contentTypePropertyTypeCreatedText: 'Name: %0%
DataType: %1%', @@ -1426,12 +1426,13 @@ export default { cssSavedText: 'Stylesheet saved without any errors', dataTypeSaved: 'Datatype saved', dictionaryItemSaved: 'Dictionary item saved', - editContentPublishedFailedByParent: 'Content could not be published, because a parent page is not published', - editContentPublishedHeader: 'Content published', - editContentPublishedText: 'and visible on the website', + editContentPublishedFailedByValidation: 'Document could not be published, but we saved it for you', + editContentPublishedFailedByParent: 'Document could not be published, because a parent page is not published', + editContentPublishedHeader: 'Document published', + editContentPublishedText: 'and is visible on the website', editBlueprintSavedHeader: 'Document Blueprint saved', editBlueprintSavedText: 'Changes have been successfully saved', - editContentSavedHeader: 'Content saved', + editContentSavedHeader: 'Document saved', editContentSavedText: 'Remember to publish to make changes visible', editContentSendToPublish: 'Sent For Approval', editContentSendToPublishText: 'Changes have been sent for approval', @@ -1493,10 +1494,11 @@ export default { cannotCopyInformation: 'Could not copy your system information to the clipboard', webhookSaved: 'Webhook saved', operationSavedHeaderReloadUser: 'Saved. To view the changes please reload your browser', - editMultiContentPublishedText: '%0% documents published and visible on the website', - editVariantPublishedText: '%0% published and visible on the website', - editMultiVariantPublishedText: '%0% documents published for languages %1% and visible on the website', + editMultiContentPublishedText: '%0% documents published and are visible on the website', + editVariantPublishedText: '%0% published and is visible on the website', + editMultiVariantPublishedText: '%0% documents published for languages %1% and are visible on the website', editContentScheduledSavedText: 'A schedule for publishing has been updated', + editContentScheduledNotSavedText: 'The schedule for publishing could not be updated', editVariantSavedText: '%0% saved', editVariantSendToPublishText: '%0% changes have been sent for approval', contentCultureUnpublished: 'Content variation %0% unpublished', diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts index 24c2a0d331a4..bb1376e6a848 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts @@ -282,6 +282,16 @@ describe('UmbLocalizeController', () => { }); }); + describe('list format', () => { + it('should return a list with conjunction', () => { + expect(controller.list(['one', 'two', 'three'], { type: 'conjunction' })).to.equal('one, two, and three'); + }); + + it('should return a list with disjunction', () => { + expect(controller.list(['one', 'two', 'three'], { type: 'disjunction' })).to.equal('one, two, or three'); + }); + }); + describe('duration', () => { it('should return a duration', () => { const now = new Date('2020-01-01T00:00:00'); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 0e8b63935a5e..28ca8df5ea61 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -941,8 +941,16 @@ export const data: Array = [ { alias: 'layouts', value: [ - { icon: 'icon-grid', isSystem: true, name: 'Grid', path: '', selected: true }, - { icon: 'icon-list', isSystem: true, name: 'Table', path: '', selected: true }, + { + icon: 'icon-grid', + name: 'Document Grid Collection View', + collectionView: 'Umb.CollectionView.Document.Grid', + }, + { + icon: 'icon-list', + name: 'Document Table Collection View', + collectionView: 'Umb.CollectionView.Document.Table', + }, ], }, { alias: 'icon', value: 'icon-layers' }, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts index df508ed15e88..f420b115fe03 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts @@ -41,6 +41,13 @@ export const detailHandlers = [ return res(ctx.status(200), ctx.json(PagedTrackedReference)); }), + rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`, 'v1.1'), (_req, res, ctx) => { + const id = _req.params.id as string; + if (!id) return res(ctx.status(400)); + + return res(ctx.status(200)); + }), + rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/umbraco-path.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/umbraco-path.function.ts index 754e07d3a947..9466adb7c34b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/umbraco-path.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/umbraco-path.function.ts @@ -1,8 +1,10 @@ // TODO: Rename to something more obvious, naming wise this can mean anything. I suggest: umbracoManagementApiPath() /** - * - * @param path + * Generates a path to an Umbraco API endpoint. + * @param {string} path - The path to the Umbraco API endpoint. + * @param {string} version - The version of the Umbraco API (default is 'v1'). + * @returns {string} The path to the Umbraco API endpoint. */ -export function umbracoPath(path: string) { - return `/umbraco/management/api/v1${path}`; +export function umbracoPath(path: string, version = 'v1') { + return `/umbraco/management/api/${version}${path}`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts index e74be197226a..098993fc23e2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts @@ -10,6 +10,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { @@ -19,6 +21,9 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { override async execute() { if (!this.args.unique) throw new Error('The document unique identifier is missing'); + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + const localize = new UmbLocalizationController(this); + const languageRepository = new UmbLanguageCollectionRepository(this._host); const { data: languageData } = await languageRepository.requestCollection({}); @@ -65,7 +70,15 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { if (options.length === 1) { const variantId = UmbVariantId.Create(documentData.variants[0]); const publishingRepository = new UmbDocumentPublishingRepository(this._host); - await publishingRepository.publish(this.args.unique, [{ variantId }]); + const { error } = await publishingRepository.publish(this.args.unique, [{ variantId }]); + if (!error) { + notificationContext.peek('positive', { + data: { + headline: localize.term('speechBubbles_editContentPublishedHeader'), + message: localize.term('speechBubbles_editContentPublishedText'), + }, + }); + } actionEventContext.dispatchEvent(event); return; } @@ -103,10 +116,24 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { if (variantIds.length) { const publishingRepository = new UmbDocumentPublishingRepository(this._host); - await publishingRepository.publish( + const { error } = await publishingRepository.publish( this.args.unique, variantIds.map((variantId) => ({ variantId })), ); + + if (!error) { + const documentVariants = documentData.variants.filter((variant) => result.selection.includes(variant.culture!)); + notificationContext.peek('positive', { + data: { + headline: localize.term('speechBubbles_editContentPublishedHeader'), + message: localize.term( + 'speechBubbles_editVariantPublishedText', + localize.list(documentVariants.map((v) => v.culture ?? v.name)), + ), + }, + }); + } + actionEventContext.dispatchEvent(event); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts index 7c2d18292601..014946c962af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts @@ -11,6 +11,7 @@ import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization- import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase { async execute() { @@ -18,6 +19,9 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< const entityType = entityContext.getEntityType(); const unique = entityContext.getUnique(); + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + const localize = new UmbLocalizationController(this); + if (!entityType) throw new Error('Entity type not found'); if (unique === undefined) throw new Error('Entity unique not found'); @@ -46,7 +50,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< updateDate: null, segment: null, scheduledPublishDate: null, - scheduledUnpublishDate: null + scheduledUnpublishDate: null, }, unique: new UmbVariantId(language.unique, null).toString(), culture: language.unique, @@ -79,11 +83,24 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< if (confirm !== false) { const variantId = new UmbVariantId(options[0].language.unique, null); const publishingRepository = new UmbDocumentPublishingRepository(this._host); + let documentCnt = 0; + for (let i = 0; i < this.selection.length; i++) { const id = this.selection[i]; - await publishingRepository.publish(id, [{ variantId }]); + const { error } = await publishingRepository.publish(id, [{ variantId }]); + + if (!error) { + documentCnt++; + } } + notificationContext.peek('positive', { + data: { + headline: localize.term('speechBubbles_editContentPublishedHeader'), + message: localize.term('speechBubbles_editMultiContentPublishedText', documentCnt), + }, + }); + eventContext.dispatchEvent(event); } return; @@ -116,13 +133,30 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< const repository = new UmbDocumentPublishingRepository(this._host); if (variantIds.length) { + let documentCnt = 0; for (const unique of this.selection) { - await repository.publish( + const { error } = await repository.publish( unique, variantIds.map((variantId) => ({ variantId })), ); - eventContext.dispatchEvent(event); + + if (!error) { + documentCnt++; + } } + + notificationContext.peek('positive', { + data: { + headline: localize.term('speechBubbles_editContentPublishedHeader'), + message: localize.term( + 'speechBubbles_editMultiVariantPublishedText', + documentCnt, + localize.list(variantIds.map((v) => v.culture ?? '')), + ), + }, + }); + + eventContext.dispatchEvent(event); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts index dff0946ba2a6..2ed98a2e05ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts @@ -8,6 +8,10 @@ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; export class UmbDocumentPublishingRepository extends UmbRepositoryBase { #init!: Promise; #publishingDataSource: UmbDocumentPublishingServerDataSource; + + /** + * @deprecated The calling workspace context should be used instead to show notifications + */ #notificationContext?: UmbNotificationContext; constructor(host: UmbControllerHost) { @@ -36,14 +40,7 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { if (!variants.length) throw new Error('variant IDs are missing'); await this.#init; - const { error } = await this.#publishingDataSource.publish(unique, variants); - - if (!error) { - const notification = { data: { message: `Document published` } }; - this.#notificationContext?.peek('positive', notification); - } - - return { error }; + return this.#publishingDataSource.publish(unique, variants); } /** @@ -62,6 +59,7 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { if (!error) { const notification = { data: { message: `Document unpublished` } }; + // TODO: Move this to the calling workspace context [JOV] this.#notificationContext?.peek('positive', notification); } @@ -76,7 +74,12 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { * @param forceRepublish * @memberof UmbDocumentPublishingRepository */ - async publishWithDescendants(id: string, variantIds: Array, includeUnpublishedDescendants: boolean, forceRepublish: boolean) { + async publishWithDescendants( + id: string, + variantIds: Array, + includeUnpublishedDescendants: boolean, + forceRepublish: boolean, + ) { if (!id) throw new Error('id is missing'); if (!variantIds) throw new Error('variant IDs are missing'); await this.#init; @@ -90,6 +93,7 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { if (!error) { const notification = { data: { message: `Document published with descendants` } }; + // TODO: Move this to the calling workspace context [JOV] this.#notificationContext?.peek('positive', notification); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts index 86d6b069b122..2d2c9940bfb7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts @@ -25,6 +25,7 @@ import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase { /** @@ -39,6 +40,8 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase { + this.#notificationContext = context; + }); } public async publish() { @@ -118,17 +125,47 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase x.variantId); + const saveData = await this.#documentWorkspaceContext.constructSaveData(variantIds); + await this.#documentWorkspaceContext.runMandatoryValidationForSaveData(saveData); + await this.#documentWorkspaceContext.askServerToValidate(saveData, variantIds); - // request reload of this entity - const structureEvent = new UmbRequestReloadStructureForEntityEvent({ entityType, unique }); - this.#eventContext?.dispatchEvent(structureEvent); - } + // TODO: Only validate the specified selection.. [NL] + return this.#documentWorkspaceContext.validateAndSubmit( + async () => { + if (!this.#documentWorkspaceContext) { + throw new Error('Document workspace context is missing'); + } + + // Save the document before scheduling + await this.#documentWorkspaceContext.performCreateOrUpdate(variantIds, saveData); + + // Schedule the document + const { error } = await this.#publishingRepository.publish(unique, variants); + if (error) { + return Promise.reject(error); + } + + const notification = { data: { message: this.#localize.term('speechBubbles_editContentScheduledSavedText') } }; + this.#notificationContext?.peek('positive', notification); + + // reload the document so all states are updated after the publish operation + await this.#documentWorkspaceContext.reload(); + this.#loadAndProcessLastPublished(); + + // request reload of this entity + const structureEvent = new UmbRequestReloadStructureForEntityEvent({ entityType, unique }); + this.#eventContext?.dispatchEvent(structureEvent); + }, + async () => { + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + notificationContext.peek('danger', { + data: { message: this.#localize.term('speechBubbles_editContentScheduledNotSavedText') }, + }); + + return Promise.reject(); + }, + ); } /** @@ -280,9 +317,8 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase variantIds.some((id) => id.culture === v.culture)); + this.#notificationContext?.peek('positive', { + data: { + headline: this.#localize.term('speechBubbles_editContentPublishedHeader'), + message: this.#localize.term( + 'speechBubbles_editVariantPublishedText', + this.#localize.list(variants.map((v) => v.culture ?? v.name)), + ), + }, + }); + // reload the document so all states are updated after the publish operation await this.#documentWorkspaceContext.reload(); this.#loadAndProcessLastPublished();