Skip to content

Commit 55506ba

Browse files
Simplify creating content from a blueprint programmatically (#19528)
* Rename `IContentService.CreateContentFromBlueprint` to `CreateBlueprintFromContent` In reality, this method is used by the core to create a blueprint from content, and not the other way around, which doesn't need new ids. This was causing confusion, so the old name has been marked as deprecated in favor of the new name. If developers want to create content from blueprints they should use `IContentBlueprintEditingService.GetScaffoldedAsync()` instead, which is what is used by the management api. * Added integration tests to verify that new block ids are generated when creating content from a blueprint * Return copy of the blueprint in `ContentBlueprintEditingService.GetScaffoldedAsync` instead of the blueprint itself * Update CreateContentFromBlueprint xml docs to mention both replacement methods * Fix tests for rich text blocks * Small re-organization * Adjusted tests that were still referencing `ContentService.CreateContentFromBlueprint` * Add default implementation to new CreateBlueprintFromContent method * Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs Co-authored-by: Andy Butland <abutland73@gmail.com> --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
1 parent b41eecf commit 55506ba

File tree

10 files changed

+388
-138
lines changed

10 files changed

+388
-138
lines changed

src/Umbraco.Core/Services/ContentBlueprintEditingService.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ public ContentBlueprintEditingService(
4545
return Task.FromResult<IContent?>(null);
4646
}
4747

48+
IContent scaffold = blueprint.DeepCloneWithResetIdentities();
49+
4850
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
49-
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
51+
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, scaffold, Constants.System.Root, new EventMessages()));
5052
scope.Complete();
5153

52-
return Task.FromResult<IContent?>(blueprint);
54+
return Task.FromResult<IContent?>(scaffold);
5355
}
5456

5557
public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
@@ -112,7 +114,7 @@ public async Task<Attempt<ContentCreateResult, ContentEditingOperationStatus>> C
112114

113115
// Create Blueprint
114116
var currentUserId = await GetUserIdAsync(userKey);
115-
IContent blueprint = ContentService.CreateContentFromBlueprint(content, name, currentUserId);
117+
IContent blueprint = ContentService.CreateBlueprintFromContent(content, name, currentUserId);
116118

117119
if (key.HasValue)
118120
{

src/Umbraco.Core/Services/ContentService.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3654,12 +3654,12 @@ public void DeleteBlueprint(IContent content, int userId = Constants.Security.Su
36543654

36553655
private static readonly string?[] ArrayOfOneNullString = { null };
36563656

3657-
public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
3657+
public IContent CreateBlueprintFromContent(
3658+
IContent blueprint,
3659+
string name,
3660+
int userId = Constants.Security.SuperUserId)
36583661
{
3659-
if (blueprint == null)
3660-
{
3661-
throw new ArgumentNullException(nameof(blueprint));
3662-
}
3662+
ArgumentNullException.ThrowIfNull(blueprint);
36633663

36643664
IContentType contentType = GetContentType(blueprint.ContentType.Alias);
36653665
var content = new Content(name, -1, contentType);
@@ -3672,15 +3672,13 @@ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int
36723672
if (blueprint.CultureInfos?.Count > 0)
36733673
{
36743674
cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
3675-
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
3675+
using ICoreScope scope = ScopeProvider.CreateCoreScope();
3676+
if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
36763677
{
3677-
if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
3678-
{
3679-
defaultCulture.Name = name;
3680-
}
3681-
3682-
scope.Complete();
3678+
defaultCulture.Name = name;
36833679
}
3680+
3681+
scope.Complete();
36843682
}
36853683

36863684
DateTime now = DateTime.Now;
@@ -3701,6 +3699,11 @@ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int
37013699
return content;
37023700
}
37033701

3702+
/// <inheritdoc />
3703+
[Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
3704+
public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
3705+
=> CreateBlueprintFromContent(blueprint, name, userId);
3706+
37043707
public IEnumerable<IContent> GetBlueprintsForContentTypes(params int[] contentTypeId)
37053708
{
37063709
using (ScopeProvider.CreateCoreScope(autoComplete: true))

src/Umbraco.Core/Services/IContentService.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,18 @@ public interface IContentService : IContentServiceBase<IContent>
5555
void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
5656

5757
/// <summary>
58-
/// Creates a new content item from a blueprint.
58+
/// Creates a blueprint from a content item.
5959
/// </summary>
60+
// TODO: Remove the default implementation when CreateContentFromBlueprint is removed.
61+
IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
62+
=> throw new NotImplementedException();
63+
64+
/// <summary>
65+
/// (Deprecated) Creates a new content item from a blueprint.
66+
/// </summary>
67+
/// <remarks>If creating content from a blueprint, use <see cref="IContentBlueprintEditingService.GetScaffoldedAsync"/>
68+
/// instead. If creating a blueprint from content use <see cref="CreateBlueprintFromContent"/> instead.</remarks>
69+
[Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
6070
IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
6171

6272
/// <summary>

tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using System;
4+
using Umbraco.Cms.Core;
5+
using Umbraco.Cms.Core.IO;
56
using Umbraco.Cms.Core.Models;
7+
using Umbraco.Cms.Core.PropertyEditors;
68
using Umbraco.Cms.Infrastructure.Serialization;
9+
using Umbraco.Cms.Tests.Common.Builders.Extensions;
710
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
811

912
namespace Umbraco.Cms.Tests.Common.Builders;
@@ -155,4 +158,100 @@ public override DataType Build()
155158

156159
return dataType;
157160
}
161+
162+
public static DataType CreateSimpleElementDataType(
163+
IIOHelper ioHelper,
164+
string editorAlias,
165+
Guid elementKey,
166+
Guid? elementSettingKey)
167+
{
168+
Dictionary<string, object> configuration = editorAlias switch
169+
{
170+
Constants.PropertyEditors.Aliases.BlockGrid => GetBlockGridBaseConfiguration(),
171+
Constants.PropertyEditors.Aliases.RichText => GetRteBaseConfiguration(),
172+
_ => [],
173+
};
174+
175+
SetBlockConfiguration(
176+
configuration,
177+
elementKey,
178+
elementSettingKey,
179+
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);
180+
181+
182+
var dataTypeBuilder = new DataTypeBuilder()
183+
.WithId(0)
184+
.WithDatabaseType(ValueStorageType.Nvarchar)
185+
.AddEditor()
186+
.WithAlias(editorAlias);
187+
188+
switch (editorAlias)
189+
{
190+
case Constants.PropertyEditors.Aliases.BlockGrid:
191+
dataTypeBuilder.WithConfigurationEditor(
192+
new BlockGridConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
193+
break;
194+
case Constants.PropertyEditors.Aliases.BlockList:
195+
dataTypeBuilder.WithConfigurationEditor(
196+
new BlockListConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
197+
break;
198+
case Constants.PropertyEditors.Aliases.RichText:
199+
dataTypeBuilder.WithConfigurationEditor(
200+
new RichTextConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
201+
break;
202+
}
203+
204+
return dataTypeBuilder.Done().Build();
205+
}
206+
207+
private static void SetBlockConfiguration(
208+
Dictionary<string, object> dictionary,
209+
Guid? elementKey,
210+
Guid? elementSettingKey,
211+
bool? allowAtRoot)
212+
{
213+
if (elementKey is null)
214+
{
215+
return;
216+
}
217+
218+
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
219+
}
220+
221+
private static Dictionary<string, object> GetBlockGridBaseConfiguration() => new() { ["gridColumns"] = 12 };
222+
223+
private static Dictionary<string, object> GetRteBaseConfiguration()
224+
{
225+
var dictionary = new Dictionary<string, object>
226+
{
227+
["maxImageSize"] = 500,
228+
["mode"] = "Classic",
229+
["toolbar"] = new[]
230+
{
231+
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent",
232+
"indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
233+
},
234+
};
235+
return dictionary;
236+
}
237+
238+
private static Dictionary<string, object> BuildBlockConfiguration(
239+
Guid? elementKey,
240+
Guid? elementSettingKey,
241+
bool? allowAtRoot)
242+
{
243+
var dictionary = new Dictionary<string, object>();
244+
if (allowAtRoot is not null)
245+
{
246+
dictionary.Add("allowAtRoot", allowAtRoot.Value);
247+
}
248+
249+
dictionary.Add("contentElementTypeKey", elementKey.ToString());
250+
if (elementSettingKey is not null)
251+
{
252+
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
253+
}
254+
255+
return dictionary;
256+
}
158257
}

tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
1919

2020
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
2121

22+
protected IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
23+
2224
protected IFileService FileService => GetRequiredService<IFileService>();
2325

2426
protected ContentService ContentService => (ContentService)GetRequiredService<IContentService>();

tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public void Delete_Blueprint()
130130
}
131131

132132
[Test]
133-
public void Create_Content_From_Blueprint()
133+
public void Create_Blueprint_From_Content()
134134
{
135135
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
136136
{
@@ -140,22 +140,21 @@ public void Create_Content_From_Blueprint()
140140
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
141141
ContentTypeService.Save(contentType);
142142

143-
var blueprint = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root);
144-
blueprint.SetValue("title", "blueprint 1");
145-
blueprint.SetValue("bodyText", "blueprint 2");
146-
blueprint.SetValue("keywords", "blueprint 3");
147-
blueprint.SetValue("description", "blueprint 4");
148-
149-
ContentService.SaveBlueprint(blueprint);
150-
151-
var fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "hello world");
152-
ContentService.Save(fromBlueprint);
153-
154-
Assert.IsTrue(fromBlueprint.HasIdentity);
155-
Assert.AreEqual("blueprint 1", fromBlueprint.Properties["title"].GetValue());
156-
Assert.AreEqual("blueprint 2", fromBlueprint.Properties["bodyText"].GetValue());
157-
Assert.AreEqual("blueprint 3", fromBlueprint.Properties["keywords"].GetValue());
158-
Assert.AreEqual("blueprint 4", fromBlueprint.Properties["description"].GetValue());
143+
var originalPage = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root);
144+
originalPage.SetValue("title", "blueprint 1");
145+
originalPage.SetValue("bodyText", "blueprint 2");
146+
originalPage.SetValue("keywords", "blueprint 3");
147+
originalPage.SetValue("description", "blueprint 4");
148+
ContentService.Save(originalPage);
149+
150+
var fromContent = ContentService.CreateBlueprintFromContent(originalPage, "hello world");
151+
ContentService.SaveBlueprint(fromContent);
152+
153+
Assert.IsTrue(fromContent.HasIdentity);
154+
Assert.AreEqual("blueprint 1", fromContent.Properties["title"]?.GetValue());
155+
Assert.AreEqual("blueprint 2", fromContent.Properties["bodyText"]?.GetValue());
156+
Assert.AreEqual("blueprint 3", fromContent.Properties["keywords"]?.GetValue());
157+
Assert.AreEqual("blueprint 4", fromContent.Properties["description"]?.GetValue());
159158
}
160159
}
161160

tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs

Lines changed: 4 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -286,104 +286,12 @@ private async Task SetupDataType(
286286
Guid elementKey,
287287
Guid? elementSettingKey)
288288
{
289-
Dictionary<string, object> configuration;
290-
switch (editorAlias)
291-
{
292-
case Constants.PropertyEditors.Aliases.BlockGrid:
293-
configuration = GetBlockGridBaseConfiguration();
294-
break;
295-
case Constants.PropertyEditors.Aliases.RichText:
296-
configuration = GetRteBaseConfiguration();
297-
break;
298-
default:
299-
configuration = new Dictionary<string, object>();
300-
break;
301-
}
302-
303-
SetBlockConfiguration(
304-
configuration,
289+
var dataType = DataTypeBuilder.CreateSimpleElementDataType(
290+
IOHelper,
291+
editorAlias,
305292
elementKey,
306-
elementSettingKey,
307-
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);
308-
309-
310-
var dataTypeBuilder = new DataTypeBuilder()
311-
.WithId(0)
312-
.WithDatabaseType(ValueStorageType.Nvarchar)
313-
.AddEditor()
314-
.WithAlias(editorAlias);
315-
316-
switch (editorAlias)
317-
{
318-
case Constants.PropertyEditors.Aliases.BlockGrid:
319-
dataTypeBuilder.WithConfigurationEditor(
320-
new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
321-
break;
322-
case Constants.PropertyEditors.Aliases.BlockList:
323-
dataTypeBuilder.WithConfigurationEditor(
324-
new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
325-
break;
326-
case Constants.PropertyEditors.Aliases.RichText:
327-
dataTypeBuilder.WithConfigurationEditor(
328-
new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
329-
break;
330-
}
331-
332-
var dataType = dataTypeBuilder.Done()
333-
.Build();
293+
elementSettingKey);
334294

335295
await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
336296
}
337-
338-
private void SetBlockConfiguration(
339-
Dictionary<string, object> dictionary,
340-
Guid? elementKey,
341-
Guid? elementSettingKey,
342-
bool? allowAtRoot)
343-
{
344-
if (elementKey is null)
345-
{
346-
return;
347-
}
348-
349-
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
350-
}
351-
352-
private Dictionary<string, object> GetBlockGridBaseConfiguration()
353-
=> new Dictionary<string, object> { ["gridColumns"] = 12 };
354-
355-
private Dictionary<string, object> GetRteBaseConfiguration()
356-
{
357-
var dictionary = new Dictionary<string, object>
358-
{
359-
["maxImageSize"] = 500,
360-
["mode"] = "Classic",
361-
["toolbar"] = new[]
362-
{
363-
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist",
364-
"outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
365-
},
366-
};
367-
return dictionary;
368-
}
369-
370-
private Dictionary<string, object> BuildBlockConfiguration(
371-
Guid? elementKey,
372-
Guid? elementSettingKey,
373-
bool? allowAtRoot)
374-
{
375-
var dictionary = new Dictionary<string, object>();
376-
if (allowAtRoot is not null)
377-
{
378-
dictionary.Add("allowAtRoot", allowAtRoot.Value);
379-
}
380-
381-
dictionary.Add("contentElementTypeKey", elementKey.ToString());
382-
if (elementSettingKey is not null)
383-
{
384-
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
385-
}
386-
387-
return dictionary;
388-
}
389297
}

0 commit comments

Comments
 (0)