Skip to content

Commit b61856b

Browse files
author
Warren Buckley
authored
Merge pull request #42 from umbraco/v3/feature/blockeditor-valueconnector
adding connector for blocklist property editor.
2 parents 9ff2829 + 99e981b commit b61856b

File tree

3 files changed

+267
-3
lines changed

3 files changed

+267
-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: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
// get all the content types used in block editor items
59+
var allContentTypes = blockEditorValue.Data.Select(x => x.ContentTypeKey)
60+
.Distinct()
61+
.ToDictionary(a => a, a =>
62+
{
63+
if (!Guid.TryParse(a, out var keyAsGuid))
64+
throw new InvalidOperationException($"Could not parse ContentTypeKey as GUID {keyAsGuid}.");
65+
return _contentTypeService.Get(keyAsGuid);
66+
});
67+
68+
//Ensure all of these content types are found
69+
if (allContentTypes.Values.Any(contentType => contentType == null))
70+
{
71+
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))}");
72+
}
73+
74+
//Ensure that these content types have dependencies added
75+
foreach (var contentType in allContentTypes.Values)
76+
{
77+
dependencies.Add(new ArtifactDependency(contentType.GetUdi(), false, ArtifactDependencyMode.Match));
78+
}
79+
80+
foreach (var block in blockEditorValue.Data)
81+
{
82+
var contentType = allContentTypes[block.ContentTypeKey];
83+
84+
foreach (var key in block.PropertyValues.Keys.ToArray())
85+
{
86+
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
87+
88+
if (propType == null)
89+
{
90+
_logger.Debug<BlockEditorValueConnector>("No property type found with alias {Key} on content type {ContentTypeAlias}.", key, contentType.Alias);
91+
continue;
92+
}
93+
94+
// fetch the right value connector from the collection of connectors, intended for use with this property type.
95+
// throws if not found - no need for a null check
96+
var propValueConnector = ValueConnectors.Get(propType);
97+
98+
// pass the value, property type and the dependencies collection to the connector to get a "artifact" value
99+
var val = block.PropertyValues[key];
100+
object parsedValue = propValueConnector.ToArtifact(val, propType, dependencies);
101+
102+
_logger.Debug<BlockEditorValueConnector>("Map {Key} value '{PropertyValue}' to '{ParsedValue}' using {PropValueConnectorType} for {PropTypeAlias}.", key, block.PropertyValues[key], parsedValue, propValueConnector.GetType(), propType.Alias);
103+
104+
parsedValue = parsedValue?.ToString();
105+
106+
block.PropertyValues[key] = parsedValue;
107+
}
108+
}
109+
110+
value = JsonConvert.SerializeObject(blockEditorValue);
111+
return (string)value;
112+
}
113+
114+
public object FromArtifact(string value, PropertyType propertyType, object currentValue)
115+
{
116+
if (string.IsNullOrWhiteSpace(value))
117+
{
118+
return value;
119+
}
120+
121+
if (value.DetectIsJson() == false)
122+
return value;
123+
124+
var blockEditorValue = JsonConvert.DeserializeObject<BlockEditorValue>(value);
125+
126+
if (blockEditorValue == null)
127+
return value;
128+
129+
var allContentTypes = blockEditorValue.Data.Select(x => x.ContentTypeKey)
130+
.Distinct()
131+
.ToDictionary(a => a, a =>
132+
{
133+
if (!Guid.TryParse(a, out var keyAsGuid))
134+
throw new InvalidOperationException($"Could not parse ContentTypeKey as GUID {keyAsGuid}.");
135+
return _contentTypeService.Get(keyAsGuid);
136+
});
137+
138+
//Ensure all of these content types are found
139+
if (allContentTypes.Values.Any(contentType => contentType == null))
140+
{
141+
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))}");
142+
}
143+
144+
foreach (var block in blockEditorValue.Data)
145+
{
146+
var contentType = allContentTypes[block.ContentTypeKey];
147+
148+
foreach (var key in block.PropertyValues.Keys.ToArray())
149+
{
150+
var innerPropertyType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
151+
152+
if (innerPropertyType == null)
153+
{
154+
_logger.Debug<BlockEditorValueConnector>("No property type found with alias {Key} on content type {ContentTypeAlias}.", key, contentType.Alias);
155+
continue;
156+
}
157+
158+
// fetch the right value connector from the collection of connectors, intended for use with this property type.
159+
// throws if not found - no need for a null check
160+
var propValueConnector = ValueConnectors.Get(innerPropertyType);
161+
162+
var propertyValue = block.PropertyValues[key];
163+
164+
if (propertyValue != null)
165+
{
166+
// pass the artifact value and property type to the connector to get a real value from the artifact
167+
var convertedValue = propValueConnector.FromArtifact(propertyValue.ToString(), innerPropertyType, null);
168+
if (convertedValue == null)
169+
{
170+
block.PropertyValues[key] = null;
171+
}
172+
else
173+
{
174+
block.PropertyValues[key] = convertedValue;
175+
}
176+
}
177+
else
178+
{
179+
block.PropertyValues[key] = propertyValue;
180+
}
181+
}
182+
}
183+
184+
return JObject.FromObject(blockEditorValue);
185+
}
186+
187+
/// <summary>
188+
/// Strongly typed representation of the stored value for a block editor value
189+
/// </summary>
190+
/// <example>
191+
/// Example JSON:
192+
/// <![CDATA[
193+
/// {
194+
/// "layout": {
195+
/// "Umbraco.BlockList": [
196+
/// {
197+
/// "udi": "umb://element/b401bb800a4a48f79786d5079bc47718"
198+
/// }
199+
/// ]
200+
/// },
201+
/// "data": [
202+
/// {
203+
/// "contentTypeKey": "5fe26fff-7163-4805-9eca-960b1f106bb9",
204+
/// "udi": "umb://element/b401bb800a4a48f79786d5079bc47718",
205+
/// "image": "umb://media/e28a0070890848079d5781774c3c5ffb",
206+
/// "text": "hero text",
207+
/// "contentpicker": "umb://document/87478d1efa66413698063f8d00fda1d1"
208+
/// }
209+
/// ]
210+
/// }
211+
/// ]]>
212+
/// </example>
213+
public class BlockEditorValue
214+
{
215+
/// <summary>
216+
/// We do not have to actually handle anything in the layout since it should only contain references to items existing as data.
217+
/// JObject is fine for transferring this over.
218+
/// </summary>
219+
[JsonProperty("layout")]
220+
public JObject Layout { get; set; }
221+
222+
/// <summary>
223+
/// This contains all the blocks created in the block editor.
224+
/// </summary>
225+
[JsonProperty("data")]
226+
public IEnumerable<Block> Data { get; set; }
227+
}
228+
229+
public class Block
230+
{
231+
[JsonProperty("contentTypeKey")]
232+
public string ContentTypeKey { get; set; }
233+
[JsonProperty("udi")]
234+
public string Udi { get; set; }
235+
236+
/// <summary>
237+
/// This is the property values defined on the block.
238+
/// 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.
239+
/// </summary>
240+
[JsonExtensionData]
241+
public IDictionary<string, object> PropertyValues { get; set; }
242+
}
243+
}
244+
}
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)