Skip to content

Commit ec4807f

Browse files
committed
Merge remote-tracking branch 'origin/dev-v3' into main-v3
2 parents cbc150a + b56c611 commit ec4807f

File tree

3 files changed

+292
-3
lines changed

3 files changed

+292
-3
lines changed

src/Umbraco.Deploy.Contrib.Connectors/Umbraco.Deploy.Contrib.Connectors.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,15 @@
161161
<ItemGroup>
162162
<Compile Include="Properties\AssemblyInfo.cs" />
163163
<Compile Include="Properties\VersionInfo.cs" />
164+
<Compile Include="ValueConnectors\BlockEditorValueConnector.cs" />
164165
<Compile Include="ValueConnectors\MultiUrlPickerValueConnector.cs" />
166+
<Compile Include="ValueConnectors\BlockListValueConnector.cs" />
165167
<Compile Include="ValueConnectors\NestedContentValueConnector.cs" />
166168
</ItemGroup>
167169
<ItemGroup>
168170
<Content Include="GridCellValueConnectors\dummy.txt" />
169171
</ItemGroup>
170-
<ItemGroup>
171-
<Folder Include="ValueConnectors\" />
172-
</ItemGroup>
172+
<ItemGroup />
173173
<ItemGroup />
174174
<ItemGroup>
175175
<None Include="packages.config" />
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Linq;
6+
using Umbraco.Core;
7+
using Umbraco.Core.Deploy;
8+
using Umbraco.Core.Logging;
9+
using Umbraco.Core.Models;
10+
using Umbraco.Core.Services;
11+
using Umbraco.Deploy.Connectors.ValueConnectors.Services;
12+
13+
namespace Umbraco.Deploy.Contrib.Connectors.ValueConnectors
14+
{
15+
/// <summary>
16+
/// A Deploy connector for BlockEditor based property editors (ie. BlockList)
17+
/// </summary>
18+
public abstract class BlockEditorValueConnector : IValueConnector
19+
{
20+
private readonly IContentTypeService _contentTypeService;
21+
private readonly Lazy<ValueConnectorCollection> _valueConnectorsLazy;
22+
private readonly ILogger _logger;
23+
24+
public virtual IEnumerable<string> PropertyEditorAliases => new[] { "Umbraco.BlockEditor" };
25+
26+
// cannot inject ValueConnectorCollection directly as it would cause a circular (recursive) dependency,
27+
// so we have to inject it lazily and use the lazy value when actually needing it
28+
private ValueConnectorCollection ValueConnectors => _valueConnectorsLazy.Value;
29+
30+
public BlockEditorValueConnector(IContentTypeService contentTypeService, Lazy<ValueConnectorCollection> valueConnectors, ILogger logger)
31+
{
32+
if (contentTypeService == null) throw new ArgumentNullException(nameof(contentTypeService));
33+
if (valueConnectors == null) throw new ArgumentNullException(nameof(valueConnectors));
34+
if (logger == null) throw new ArgumentNullException(nameof(logger));
35+
_contentTypeService = contentTypeService;
36+
_valueConnectorsLazy = valueConnectors;
37+
_logger = logger;
38+
}
39+
40+
public string ToArtifact(object value, PropertyType propertyType, ICollection<ArtifactDependency> dependencies)
41+
{
42+
var svalue = value as string;
43+
44+
// nested values will arrive here as JObject - convert to string to enable reuse of same code as when non-nested.
45+
if (value is JObject)
46+
svalue = value.ToString();
47+
48+
if (string.IsNullOrWhiteSpace(svalue))
49+
return null;
50+
51+
if (svalue.DetectIsJson() == false)
52+
return null;
53+
var blockEditorValue = JsonConvert.DeserializeObject<BlockEditorValue>(svalue);
54+
55+
if (blockEditorValue == null)
56+
return null;
57+
58+
var allBlocks = blockEditorValue.Content.Concat(blockEditorValue.Settings);
59+
60+
// get all the content types used in block editor items
61+
var allContentTypes = allBlocks.Select(x => x.ContentTypeKey)
62+
.Distinct()
63+
.ToDictionary(a => a, a =>
64+
{
65+
if (!Guid.TryParse(a, out var keyAsGuid))
66+
throw new InvalidOperationException($"Could not parse ContentTypeKey as GUID {keyAsGuid}.");
67+
return _contentTypeService.Get(keyAsGuid);
68+
});
69+
70+
//Ensure all of these content types are found
71+
if (allContentTypes.Values.Any(contentType => contentType == null))
72+
{
73+
throw new InvalidOperationException($"Could not resolve these content types for the Block Editor property: {string.Join(",", allContentTypes.Where(x => x.Value == null).Select(x => x.Key))}");
74+
}
75+
76+
//Ensure that these content types have dependencies added
77+
foreach (var contentType in allContentTypes.Values)
78+
{
79+
dependencies.Add(new ArtifactDependency(contentType.GetUdi(), false, ArtifactDependencyMode.Match));
80+
}
81+
82+
foreach (var block in allBlocks)
83+
{
84+
var contentType = allContentTypes[block.ContentTypeKey];
85+
86+
foreach (var key in block.PropertyValues.Keys.ToArray())
87+
{
88+
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
89+
90+
if (propType == null)
91+
{
92+
_logger.Debug<BlockEditorValueConnector>("No property type found with alias {Key} on content type {ContentTypeAlias}.", key, contentType.Alias);
93+
continue;
94+
}
95+
96+
// fetch the right value connector from the collection of connectors, intended for use with this property type.
97+
// throws if not found - no need for a null check
98+
var propValueConnector = ValueConnectors.Get(propType);
99+
100+
// pass the value, property type and the dependencies collection to the connector to get a "artifact" value
101+
var val = block.PropertyValues[key];
102+
object parsedValue = propValueConnector.ToArtifact(val, propType, dependencies);
103+
104+
_logger.Debug<BlockEditorValueConnector>("Map {Key} value '{PropertyValue}' to '{ParsedValue}' using {PropValueConnectorType} for {PropTypeAlias}.", key, block.PropertyValues[key], parsedValue, propValueConnector.GetType(), propType.Alias);
105+
106+
parsedValue = parsedValue?.ToString();
107+
108+
block.PropertyValues[key] = parsedValue;
109+
}
110+
}
111+
112+
value = JsonConvert.SerializeObject(blockEditorValue);
113+
return (string)value;
114+
}
115+
116+
public object FromArtifact(string value, PropertyType propertyType, object currentValue)
117+
{
118+
if (string.IsNullOrWhiteSpace(value))
119+
{
120+
return value;
121+
}
122+
123+
if (value.DetectIsJson() == false)
124+
return value;
125+
126+
var blockEditorValue = JsonConvert.DeserializeObject<BlockEditorValue>(value);
127+
128+
if (blockEditorValue == null)
129+
return value;
130+
131+
var allBlocks = blockEditorValue.Content.Concat(blockEditorValue.Settings);
132+
133+
var allContentTypes = allBlocks.Select(x => x.ContentTypeKey)
134+
.Distinct()
135+
.ToDictionary(a => a, a =>
136+
{
137+
if (!Guid.TryParse(a, out var keyAsGuid))
138+
throw new InvalidOperationException($"Could not parse ContentTypeKey as GUID {keyAsGuid}.");
139+
return _contentTypeService.Get(keyAsGuid);
140+
});
141+
142+
//Ensure all of these content types are found
143+
if (allContentTypes.Values.Any(contentType => contentType == null))
144+
{
145+
throw new InvalidOperationException($"Could not resolve these content types for the Block Editor property: {string.Join(",", allContentTypes.Where(x => x.Value == null).Select(x => x.Key))}");
146+
}
147+
148+
foreach (var block in allBlocks)
149+
{
150+
var contentType = allContentTypes[block.ContentTypeKey];
151+
152+
foreach (var key in block.PropertyValues.Keys.ToArray())
153+
{
154+
var innerPropertyType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
155+
156+
if (innerPropertyType == null)
157+
{
158+
_logger.Debug<BlockEditorValueConnector>("No property type found with alias {Key} on content type {ContentTypeAlias}.", key, contentType.Alias);
159+
continue;
160+
}
161+
162+
// fetch the right value connector from the collection of connectors, intended for use with this property type.
163+
// throws if not found - no need for a null check
164+
var propValueConnector = ValueConnectors.Get(innerPropertyType);
165+
166+
var propertyValue = block.PropertyValues[key];
167+
168+
if (propertyValue != null)
169+
{
170+
// pass the artifact value and property type to the connector to get a real value from the artifact
171+
var convertedValue = propValueConnector.FromArtifact(propertyValue.ToString(), innerPropertyType, null);
172+
if (convertedValue == null)
173+
{
174+
block.PropertyValues[key] = null;
175+
}
176+
else
177+
{
178+
block.PropertyValues[key] = convertedValue;
179+
}
180+
}
181+
else
182+
{
183+
block.PropertyValues[key] = propertyValue;
184+
}
185+
}
186+
}
187+
188+
return JObject.FromObject(blockEditorValue);
189+
}
190+
191+
/// <summary>
192+
/// Strongly typed representation of the stored value for a block editor value
193+
/// </summary>
194+
/// <example>
195+
/// Example JSON:
196+
/// <![CDATA[
197+
/// {
198+
/// "layout": {
199+
/// "Umbraco.BlockList": [
200+
/// {
201+
/// "contentUdi": "umb://element/b401bb800a4a48f79786d5079bc47718"
202+
/// }
203+
/// ]
204+
/// },
205+
/// "contentData": [
206+
/// {
207+
/// "contentTypeKey": "5fe26fff-7163-4805-9eca-960b1f106bb9",
208+
/// "udi": "umb://element/b401bb800a4a48f79786d5079bc47718",
209+
/// "image": "umb://media/e28a0070890848079d5781774c3c5ffb",
210+
/// "text": "hero text",
211+
/// "contentpicker": "umb://document/87478d1efa66413698063f8d00fda1d1"
212+
/// }
213+
/// ],
214+
/// "settingsData": [
215+
/// {
216+
/// "contentTypeKey": "2e6094ea-7bca-4b7c-a223-375254a194f4",
217+
/// "udi": "umb://element/499cf69f00c84227a59ca10fb4ae4c9a",
218+
/// "textColor": "",
219+
/// "containerWidth": "standard",
220+
/// "textWidth": [],
221+
/// "height": [],
222+
/// "overlayStrength": [],
223+
/// "textAlignment": "left",
224+
/// "verticalTextAlignment": "top",
225+
/// "animate": "0"
226+
/// }
227+
/// ]
228+
/// }
229+
/// ]]>
230+
/// </example>
231+
public class BlockEditorValue
232+
{
233+
/// <summary>
234+
/// We do not have to actually handle anything in the layout since it should only contain references to items existing as data.
235+
/// JObject is fine for transferring this over.
236+
/// </summary>
237+
[JsonProperty("layout")]
238+
public JObject Layout { get; set; }
239+
240+
/// <summary>
241+
/// This contains all the blocks created in the block editor.
242+
/// </summary>
243+
[JsonProperty("contentData")]
244+
public IEnumerable<Block> Content { get; set; }
245+
246+
/// <summary>
247+
/// This contains the settings associated with the block editor.
248+
/// </summary>
249+
[JsonProperty("settingsData")]
250+
public IEnumerable<Block> Settings { get; set; }
251+
}
252+
253+
public class Block
254+
{
255+
[JsonProperty("contentTypeKey")]
256+
public string ContentTypeKey { get; set; }
257+
258+
[JsonProperty("udi")]
259+
public string Udi { get; set; }
260+
261+
/// <summary>
262+
/// This is the property values defined on the block.
263+
/// These can be anything so we have to use a dictionary to represent them and JsonExtensionData attribute ensures all otherwise unmapped properties are stored here.
264+
/// </summary>
265+
[JsonExtensionData]
266+
public IDictionary<string, object> PropertyValues { get; set; }
267+
}
268+
}
269+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Umbraco.Core.Logging;
4+
using Umbraco.Core.Services;
5+
using Umbraco.Deploy.Connectors.ValueConnectors.Services;
6+
7+
namespace Umbraco.Deploy.Contrib.Connectors.ValueConnectors
8+
{
9+
/// <summary>
10+
/// A Deploy connector for the BlockList property editor
11+
/// </summary>
12+
public class BlockListValueConnector : BlockEditorValueConnector
13+
{
14+
public override IEnumerable<string> PropertyEditorAliases => new[] { "Umbraco.BlockList" };
15+
16+
public BlockListValueConnector(IContentTypeService contentTypeService, Lazy<ValueConnectorCollection> valueConnectors, ILogger logger)
17+
: base(contentTypeService, valueConnectors, logger)
18+
{ }
19+
}
20+
}

0 commit comments

Comments
 (0)