Skip to content

Commit 29c0151

Browse files
kjacAndyButlandiOvergaard
authored
Scaffold content for content templates serverside (#19054)
* Scaffold content for content templates serverside * Generated client types and methods from API. * Retrieve scaffolded blueprint when creating documents from a blueprint. * Use introduced helper method on existing read. * Cleaned up imports. * feat: moves scaffold service logic to data source and make shallow repository method * feat: follows UmbDataSourceResponse interface and reorders public/private methods * Bumped version to 15.4.0-r2. --------- Co-authored-by: Andy Butland <[email protected]> Co-authored-by: Jacob Overgaard <[email protected]>
1 parent cc9c33b commit 29c0151

File tree

12 files changed

+280
-38
lines changed

12 files changed

+280
-38
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
6+
using Umbraco.Cms.Core.Mapping;
7+
using Umbraco.Cms.Core.Models;
8+
using Umbraco.Cms.Core.Services;
9+
using Umbraco.Cms.Web.Common.Authorization;
10+
11+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint;
12+
13+
[ApiVersion("1.0")]
14+
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
15+
public class ScaffoldDocumentBlueprintController : DocumentBlueprintControllerBase
16+
{
17+
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
18+
private readonly IUmbracoMapper _umbracoMapper;
19+
20+
public ScaffoldDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IUmbracoMapper umbracoMapper)
21+
{
22+
_contentBlueprintEditingService = contentBlueprintEditingService;
23+
_umbracoMapper = umbracoMapper;
24+
}
25+
26+
[HttpGet("{id:guid}/scaffold")]
27+
[MapToApiVersion("1.0")]
28+
[ProducesResponseType(typeof(DocumentBlueprintResponseModel), StatusCodes.Status200OK)]
29+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
30+
public async Task<IActionResult> Scaffold(CancellationToken cancellationToken, Guid id)
31+
{
32+
IContent? blueprint = await _contentBlueprintEditingService.GetScaffoldedAsync(id);
33+
return blueprint is not null
34+
? Ok(_umbracoMapper.Map<DocumentBlueprintResponseModel>(blueprint))
35+
: DocumentBlueprintNotFound();
36+
}
37+
}

src/Umbraco.Cms.Api.Management/OpenApi.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3485,6 +3485,66 @@
34853485
]
34863486
}
34873487
},
3488+
"/umbraco/management/api/v1/document-blueprint/{id}/scaffold": {
3489+
"get": {
3490+
"tags": [
3491+
"Document Blueprint"
3492+
],
3493+
"operationId": "GetDocumentBlueprintByIdScaffold",
3494+
"parameters": [
3495+
{
3496+
"name": "id",
3497+
"in": "path",
3498+
"required": true,
3499+
"schema": {
3500+
"type": "string",
3501+
"format": "uuid"
3502+
}
3503+
}
3504+
],
3505+
"responses": {
3506+
"200": {
3507+
"description": "OK",
3508+
"content": {
3509+
"application/json": {
3510+
"schema": {
3511+
"oneOf": [
3512+
{
3513+
"$ref": "#/components/schemas/DocumentBlueprintResponseModel"
3514+
}
3515+
]
3516+
}
3517+
}
3518+
}
3519+
},
3520+
"404": {
3521+
"description": "Not Found",
3522+
"content": {
3523+
"application/json": {
3524+
"schema": {
3525+
"oneOf": [
3526+
{
3527+
"$ref": "#/components/schemas/ProblemDetails"
3528+
}
3529+
]
3530+
}
3531+
}
3532+
}
3533+
},
3534+
"401": {
3535+
"description": "The resource is protected and requires an authentication token"
3536+
},
3537+
"403": {
3538+
"description": "The authenticated user does not have access to this resource"
3539+
}
3540+
},
3541+
"security": [
3542+
{
3543+
"Backoffice User": [ ]
3544+
}
3545+
]
3546+
}
3547+
},
34883548
"/umbraco/management/api/v1/document-blueprint/folder": {
34893549
"post": {
34903550
"tags": [

src/Umbraco.Core/Services/ContentBlueprintEditingService.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Options;
33
using Umbraco.Cms.Core.Configuration.Models;
4+
using Umbraco.Cms.Core.Events;
45
using Umbraco.Cms.Core.Models;
56
using Umbraco.Cms.Core.Models.ContentEditing;
7+
using Umbraco.Cms.Core.Notifications;
68
using Umbraco.Cms.Core.PropertyEditors;
79
using Umbraco.Cms.Core.Scoping;
810
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -35,6 +37,21 @@ public ContentBlueprintEditingService(
3537
return await Task.FromResult(blueprint);
3638
}
3739

40+
public Task<IContent?> GetScaffoldedAsync(Guid key)
41+
{
42+
IContent? blueprint = ContentService.GetBlueprintById(key);
43+
if (blueprint is null)
44+
{
45+
return Task.FromResult<IContent?>(null);
46+
}
47+
48+
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
49+
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
50+
scope.Complete();
51+
52+
return Task.FromResult<IContent?>(blueprint);
53+
}
54+
3855
public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
3956
{
4057
IContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey);

src/Umbraco.Core/Services/IContentBlueprintEditingService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public interface IContentBlueprintEditingService
88
{
99
Task<IContent?> GetAsync(Guid key);
1010

11+
Task<IContent?> GetScaffoldedAsync(Guid key) => Task.FromResult<IContent?>(null);
12+
1113
Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(
1214
Guid contentTypeKey,
1315
int skip,

src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts

Lines changed: 22 additions & 1 deletion
Large diffs are not rendered by default.

src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3523,6 +3523,12 @@ export type PutDocumentBlueprintByIdMoveData = {
35233523

35243524
export type PutDocumentBlueprintByIdMoveResponse = (string);
35253525

3526+
export type GetDocumentBlueprintByIdScaffoldData = {
3527+
id: string;
3528+
};
3529+
3530+
export type GetDocumentBlueprintByIdScaffoldResponse = ((DocumentBlueprintResponseModel));
3531+
35263532
export type PostDocumentBlueprintFolderData = {
35273533
requestBody?: (CreateFolderRequestModel);
35283534
};

src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.repository.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,23 @@ import { UMB_DOCUMENT_BLUEPRINT_DETAIL_STORE_CONTEXT } from './document-blueprin
44
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
55
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
66

7-
export class UmbDocumentBlueprintDetailRepository extends UmbDetailRepositoryBase<UmbDocumentBlueprintDetailModel> {
7+
export class UmbDocumentBlueprintDetailRepository extends UmbDetailRepositoryBase<
8+
UmbDocumentBlueprintDetailModel,
9+
UmbDocumentBlueprintServerDataSource
10+
> {
811
constructor(host: UmbControllerHost) {
912
super(host, UmbDocumentBlueprintServerDataSource, UMB_DOCUMENT_BLUEPRINT_DETAIL_STORE_CONTEXT);
1013
}
14+
15+
/**
16+
* Gets an existing document blueprint by its unique identifier for scaffolding purposes, i.e. to create a new document based on an existing blueprint.
17+
* @param {string} unique - The unique identifier of the document blueprint.
18+
* @returns {UmbRepositoryResponse<UmbDocumentBlueprintDetailModel>} - The document blueprint data.
19+
* @memberof UmbDocumentBlueprintDetailRepository
20+
*/
21+
scaffoldByUnique(unique: string) {
22+
return this.detailDataSource.scaffoldByUnique(unique);
23+
}
1124
}
1225

1326
export { UmbDocumentBlueprintDetailRepository as api };

src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { UmbDocumentBlueprintDetailModel } from '../../types.js';
22
import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE } from '../../entity.js';
33
import { UmbId } from '@umbraco-cms/backoffice/id';
4-
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
4+
import type { UmbDataSourceResponse, UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
55
import type {
66
CreateDocumentBlueprintRequestModel,
7+
DocumentBlueprintResponseModel,
78
UpdateDocumentBlueprintRequestModel,
89
} from '@umbraco-cms/backoffice/external/backend-api';
910
import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api';
@@ -74,7 +75,7 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
7475
* @returns {*}
7576
* @memberof UmbDocumentBlueprintServerDataSource
7677
*/
77-
async read(unique: string) {
78+
async read(unique: string): Promise<UmbDataSourceResponse<UmbDocumentBlueprintDetailModel>> {
7879
if (!unique) throw new Error('Unique is missing');
7980

8081
const { data, error } = await tryExecuteAndNotify(
@@ -86,35 +87,24 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
8687
return { error };
8788
}
8889

89-
// TODO: make data mapper to prevent errors
90-
const document: UmbDocumentBlueprintDetailModel = {
91-
entityType: UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
92-
unique: data.id,
93-
values: data.values.map((value) => {
94-
return {
95-
editorAlias: value.editorAlias,
96-
culture: value.culture || null,
97-
segment: value.segment || null,
98-
alias: value.alias,
99-
value: value.value,
100-
};
101-
}),
102-
variants: data.variants.map((variant) => {
103-
return {
104-
state: variant.state,
105-
culture: variant.culture || null,
106-
segment: variant.segment || null,
107-
name: variant.name,
108-
publishDate: variant.publishDate || null,
109-
createDate: variant.createDate,
110-
updateDate: variant.updateDate,
111-
};
112-
}),
113-
documentType: {
114-
unique: data.documentType.id,
115-
collection: data.documentType.collection ? { unique: data.documentType.collection.id } : null,
116-
},
117-
};
90+
const document = this.#createDocumentBlueprintDetailModel(data);
91+
92+
return { data: document };
93+
}
94+
95+
async scaffoldByUnique(unique: string): Promise<UmbDataSourceResponse<UmbDocumentBlueprintDetailModel>> {
96+
if (!unique) throw new Error('Unique is missing');
97+
98+
const { data, error } = await tryExecuteAndNotify(
99+
this.#host,
100+
DocumentBlueprintService.getDocumentBlueprintByIdScaffold({ id: unique }),
101+
);
102+
103+
if (error || !data) {
104+
return { error };
105+
}
106+
107+
const document = this.#createDocumentBlueprintDetailModel(data);
118108

119109
return { data: document };
120110
}
@@ -196,4 +186,35 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
196186
// TODO: update to delete when implemented
197187
return tryExecuteAndNotify(this.#host, DocumentBlueprintService.deleteDocumentBlueprintById({ id: unique }));
198188
}
189+
190+
#createDocumentBlueprintDetailModel(data: DocumentBlueprintResponseModel): UmbDocumentBlueprintDetailModel {
191+
return {
192+
entityType: UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
193+
unique: data.id,
194+
values: data.values.map((value) => {
195+
return {
196+
editorAlias: value.editorAlias,
197+
culture: value.culture || null,
198+
segment: value.segment || null,
199+
alias: value.alias,
200+
value: value.value,
201+
};
202+
}),
203+
variants: data.variants.map((variant) => {
204+
return {
205+
state: variant.state,
206+
culture: variant.culture || null,
207+
segment: variant.segment || null,
208+
name: variant.name,
209+
publishDate: variant.publishDate || null,
210+
createDate: variant.createDate,
211+
updateDate: variant.updateDate,
212+
};
213+
}),
214+
documentType: {
215+
unique: data.documentType.id,
216+
collection: data.documentType.collection ? { unique: data.documentType.collection.id } : null,
217+
},
218+
};
219+
}
199220
}

src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,16 @@ export class UmbDocumentWorkspaceContext
211211
async create(parent: UmbEntityModel, documentTypeUnique: string, blueprintUnique?: string) {
212212
if (blueprintUnique) {
213213
const blueprintRepository = new UmbDocumentBlueprintDetailRepository(this);
214-
const { data } = await blueprintRepository.requestByUnique(blueprintUnique);
214+
const { data } = await blueprintRepository.scaffoldByUnique(blueprintUnique);
215+
216+
if (!data) throw new Error('Blueprint data is missing');
215217

216218
return this.createScaffold({
217219
parent,
218220
preset: {
219-
documentType: data?.documentType,
220-
values: data?.values,
221-
variants: data?.variants as Array<UmbDocumentVariantModel>,
221+
documentType: data.documentType,
222+
values: data.values,
223+
variants: data.variants as Array<UmbDocumentVariantModel>,
222224
},
223225
});
224226
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using NUnit.Framework;
2+
using Umbraco.Cms.Core.Events;
3+
using Umbraco.Cms.Core.Notifications;
4+
using Umbraco.Cms.Tests.Integration.Attributes;
5+
6+
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
7+
8+
public partial class ContentBlueprintEditingServiceTests
9+
{
10+
public static void AddScaffoldedNotificationHandler(IUmbracoBuilder builder)
11+
=> builder.AddNotificationHandler<ContentScaffoldedNotification, ContentScaffoldedNotificationHandler>();
12+
13+
[TestCase(true)]
14+
[TestCase(false)]
15+
[ConfigureBuilder(ActionName = nameof(AddScaffoldedNotificationHandler))]
16+
public async Task Can_Get_Scaffold(bool variant)
17+
{
18+
var blueprint = await (variant ? CreateVariantContentBlueprint() : CreateInvariantContentBlueprint());
19+
try
20+
{
21+
ContentScaffoldedNotificationHandler.ContentScaffolded = notification =>
22+
{
23+
foreach (var propertyValue in notification.Scaffold.Properties.SelectMany(property => property.Values))
24+
{
25+
propertyValue.EditedValue += " scaffolded edited";
26+
propertyValue.PublishedValue += " scaffolded published";
27+
}
28+
};
29+
var result = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Key);
30+
Assert.IsNotNull(result);
31+
Assert.AreEqual(blueprint.Key, result.Key);
32+
33+
var propertyValues = result.Properties.SelectMany(property => property.Values).ToArray();
34+
Assert.IsNotEmpty(propertyValues);
35+
Assert.Multiple(() =>
36+
{
37+
Assert.IsTrue(propertyValues.All(value => value.EditedValue is string stringValue && stringValue.EndsWith(" scaffolded edited")));
38+
Assert.IsTrue(propertyValues.All(value => value.PublishedValue is string stringValue && stringValue.EndsWith(" scaffolded published")));
39+
});
40+
}
41+
finally
42+
{
43+
ContentScaffoldedNotificationHandler.ContentScaffolded = null;
44+
}
45+
}
46+
47+
[Test]
48+
public async Task Cannot_Get_Non_Existing_Scaffold()
49+
{
50+
var result = await ContentBlueprintEditingService.GetScaffoldedAsync(Guid.NewGuid());
51+
Assert.IsNull(result);
52+
}
53+
54+
public class ContentScaffoldedNotificationHandler : INotificationHandler<ContentScaffoldedNotification>
55+
{
56+
public static Action<ContentScaffoldedNotification>? ContentScaffolded { get; set; }
57+
58+
public void Handle(ContentScaffoldedNotification notification) => ContentScaffolded?.Invoke(notification);
59+
}
60+
}

0 commit comments

Comments
 (0)