Skip to content

Commit a3d6b4f

Browse files
madsrasmussenkjacnielslyngsoe
authored
Entity Data Picker: Data editor and value converter (#20661)
* change property value to an object * add const for picker data source type * Add value editor and converter server-side * register schema for property editor + move settings ui --------- Co-authored-by: kjac <[email protected]> Co-authored-by: Niels Lyngsø <[email protected]>
1 parent 1615740 commit a3d6b4f

File tree

16 files changed

+365
-42
lines changed

16 files changed

+365
-42
lines changed

src/Umbraco.Core/Constants-PropertyEditors.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ public static class Aliases
9090
/// </summary>
9191
public const string DateOnly = "Umbraco.DateOnly";
9292

93+
/// <summary>
94+
/// Entity Data Picker
95+
/// </summary>
96+
public const string EntityDataPicker = "Umbraco.EntityDataPicker";
97+
9398
/// <summary>
9499
/// Time Only.
95100
/// </summary>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
public sealed class EntityDataPickerValue
4+
{
5+
public required IEnumerable<string> Ids { get; set; }
6+
7+
public required string DataSource { get; set; }
8+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Umbraco.Cms.Core.PropertyEditors;
2+
3+
public sealed class EntityDataPickerConfiguration
4+
{
5+
[ConfigurationField("validationLimit")]
6+
public NumberRange ValidationLimit { get; set; } = new();
7+
8+
[ConfigurationField("umbEditorDataSource")]
9+
public string DataSource { get; set; } = string.Empty;
10+
11+
public class NumberRange
12+
{
13+
public int? Min { get; set; }
14+
15+
public int? Max { get; set; }
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Umbraco.Cms.Core.IO;
2+
3+
namespace Umbraco.Cms.Core.PropertyEditors;
4+
5+
internal sealed class EntityDataPickerConfigurationEditor : ConfigurationEditor<EntityDataPickerConfiguration>
6+
{
7+
public EntityDataPickerConfigurationEditor(IIOHelper ioHelper)
8+
: base(ioHelper)
9+
{
10+
}
11+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Umbraco.Cms.Core.IO;
3+
using Umbraco.Cms.Core.Models;
4+
using Umbraco.Cms.Core.Models.Validation;
5+
using Umbraco.Cms.Core.PropertyEditors.Validation;
6+
using Umbraco.Cms.Core.Serialization;
7+
using Umbraco.Cms.Core.Services;
8+
using Umbraco.Cms.Core.Strings;
9+
using Umbraco.Extensions;
10+
11+
namespace Umbraco.Cms.Core.PropertyEditors;
12+
13+
/// <summary>
14+
/// Represents an entity data picker property editor.
15+
/// </summary>
16+
[DataEditor(
17+
Constants.PropertyEditors.Aliases.EntityDataPicker,
18+
ValueType = ValueTypes.Json,
19+
ValueEditorIsReusable = true)]
20+
internal sealed class EntityDataPickerPropertyEditor : DataEditor
21+
{
22+
private readonly IIOHelper _ioHelper;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="EntityDataPickerPropertyEditor" /> class.
26+
/// </summary>
27+
public EntityDataPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper)
28+
: base(dataValueEditorFactory)
29+
{
30+
_ioHelper = ioHelper;
31+
SupportsReadOnly = true;
32+
}
33+
34+
/// <inheritdoc />
35+
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
36+
37+
/// <inheritdoc />
38+
protected override IDataValueEditor CreateValueEditor()
39+
=> DataValueEditorFactory.Create<EntityDataPickerPropertyValueEditor>(Attribute!);
40+
41+
/// <inheritdoc />
42+
protected override IConfigurationEditor CreateConfigurationEditor() => new EntityDataPickerConfigurationEditor(_ioHelper);
43+
44+
/// <summary>
45+
/// Defines the value editor for the entity data picker property editor.
46+
/// </summary>
47+
internal sealed class EntityDataPickerPropertyValueEditor : DataValueEditor
48+
{
49+
/// <summary>
50+
/// Initializes a new instance of the <see cref="EntityDataPickerPropertyValueEditor"/> class.
51+
/// </summary>
52+
public EntityDataPickerPropertyValueEditor(
53+
IShortStringHelper shortStringHelper,
54+
IJsonSerializer jsonSerializer,
55+
IIOHelper ioHelper,
56+
DataEditorAttribute attribute,
57+
ILocalizedTextService localizedTextService)
58+
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
59+
{
60+
var validators = new TypedJsonValidatorRunner<EntityDataPickerDto, EntityDataPickerConfiguration>(
61+
jsonSerializer,
62+
new MinMaxValidator(localizedTextService));
63+
64+
Validators.Add(validators);
65+
}
66+
67+
/// <summary>
68+
/// Validates the min/max configuration for the entity data picker property editor.
69+
/// </summary>
70+
internal sealed class MinMaxValidator : ITypedJsonValidator<EntityDataPickerDto, EntityDataPickerConfiguration>
71+
{
72+
private readonly ILocalizedTextService _localizedTextService;
73+
74+
/// <summary>
75+
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
76+
/// </summary>
77+
public MinMaxValidator(ILocalizedTextService localizedTextService) =>
78+
_localizedTextService = localizedTextService;
79+
80+
/// <inheritdoc/>
81+
public IEnumerable<ValidationResult> Validate(
82+
EntityDataPickerDto? data,
83+
EntityDataPickerConfiguration? configuration,
84+
string? valueType,
85+
PropertyValidationContext validationContext)
86+
{
87+
var validationResults = new List<ValidationResult>();
88+
89+
if (data is null || configuration is null)
90+
{
91+
return validationResults;
92+
}
93+
94+
if (configuration.ValidationLimit.Min is not null
95+
&& data.Ids.Length < configuration.ValidationLimit.Min)
96+
{
97+
validationResults.Add(new ValidationResult(
98+
_localizedTextService.Localize(
99+
"validation",
100+
"entriesShort",
101+
[configuration.ValidationLimit.Min.ToString(), (configuration.ValidationLimit.Min - data.Ids.Length).ToString()]),
102+
["value"]));
103+
}
104+
105+
if (configuration.ValidationLimit.Max is not null
106+
&& data.Ids.Length > configuration.ValidationLimit.Max)
107+
{
108+
validationResults.Add(new ValidationResult(
109+
_localizedTextService.Localize(
110+
"validation",
111+
"entriesExceed",
112+
[configuration.ValidationLimit.Max.ToString(), (data.Ids.Length - configuration.ValidationLimit.Max).ToString()
113+
]),
114+
["value"]));
115+
}
116+
117+
return validationResults;
118+
}
119+
}
120+
}
121+
122+
internal sealed class EntityDataPickerDto
123+
{
124+
public string[] Ids { get; set; } = [];
125+
}
126+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Umbraco.Cms.Core.Models;
2+
using Umbraco.Cms.Core.Models.PublishedContent;
3+
using Umbraco.Cms.Core.Serialization;
4+
5+
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
6+
7+
// NOTE: this class is made public on purpose because all value converters should be public
8+
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
9+
public sealed class EntityDataPickerValueConverter : PropertyValueConverterBase
10+
{
11+
private readonly IJsonSerializer _jsonSerializer;
12+
13+
public EntityDataPickerValueConverter(IJsonSerializer jsonSerializer)
14+
=> _jsonSerializer = jsonSerializer;
15+
16+
public override bool IsConverter(IPublishedPropertyType propertyType)
17+
=> Constants.PropertyEditors.Aliases.EntityDataPicker.Equals(propertyType.EditorAlias);
18+
19+
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
20+
=> typeof(EntityDataPickerValue);
21+
22+
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
23+
=> PropertyCacheLevel.Element;
24+
25+
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
26+
{
27+
if (source is not string sourceString
28+
|| propertyType.DataType.ConfigurationObject is not EntityDataPickerConfiguration dataTypeConfiguration)
29+
{
30+
return null;
31+
}
32+
33+
EntityDataPickerDto? dto = _jsonSerializer.Deserialize<EntityDataPickerDto>(sourceString);
34+
return dto is not null
35+
? new EntityDataPickerValue { Ids = dto.Ids, DataSource = dataTypeConfiguration.DataSource }
36+
: null;
37+
}
38+
39+
private class EntityDataPickerDto
40+
{
41+
public required string[] Ids { get; init; }
42+
}
43+
}

src/Umbraco.Web.UI.Client/examples/picker-data-source/index.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { UMB_PICKER_DATA_SOURCE_TYPE } from '@umbraco-cms/backoffice/picker-data-source';
2+
13
export const manifests: Array<UmbExtensionManifest> = [
24
{
35
type: 'propertyEditorDataSource',
4-
dataSourceType: 'picker',
6+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
57
alias: 'Umb.PropertyEditorDataSource.CustomPickerCollection',
68
name: 'Custom Picker Collection Data Source',
79
api: () => import('./example-custom-picker-collection-data-source.js'),
@@ -13,7 +15,7 @@ export const manifests: Array<UmbExtensionManifest> = [
1315
},
1416
{
1517
type: 'propertyEditorDataSource',
16-
dataSourceType: 'picker',
18+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
1719
alias: 'Umb.PropertyEditorDataSource.CustomPickerTree',
1820
name: 'Custom Picker Tree Data Source',
1921
api: () => import('./example-custom-picker-tree-data-source.js'),
@@ -25,7 +27,7 @@ export const manifests: Array<UmbExtensionManifest> = [
2527
},
2628
{
2729
type: 'propertyEditorDataSource',
28-
dataSourceType: 'picker',
30+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
2931
alias: 'Umb.PropertyEditorDataSource.DocumentPicker',
3032
name: 'Document Picker Data Source',
3133
api: () => import('./example-document-picker-data-source.js'),
@@ -53,7 +55,7 @@ export const manifests: Array<UmbExtensionManifest> = [
5355
},
5456
{
5557
type: 'propertyEditorDataSource',
56-
dataSourceType: 'picker',
58+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
5759
alias: 'Umb.PropertyEditorDataSource.MediaPicker',
5860
name: 'Media Picker Data Source',
5961
api: () => import('./example-media-picker-data-source.js'),
@@ -65,7 +67,7 @@ export const manifests: Array<UmbExtensionManifest> = [
6567
},
6668
{
6769
type: 'propertyEditorDataSource',
68-
dataSourceType: 'picker',
70+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
6971
alias: 'Umb.PropertyEditorDataSource.LanguagePicker',
7072
name: 'Language Picker Data Source',
7173
api: () => import('./example-language-picker-data-source.js'),
@@ -77,7 +79,7 @@ export const manifests: Array<UmbExtensionManifest> = [
7779
},
7880
{
7981
type: 'propertyEditorDataSource',
80-
dataSourceType: 'picker',
82+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
8183
alias: 'Umb.PropertyEditorDataSource.WebhookPicker',
8284
name: 'Webhook Picker Data Source',
8385
api: () => import('./example-webhook-picker-data-source.js'),
@@ -89,7 +91,7 @@ export const manifests: Array<UmbExtensionManifest> = [
8991
},
9092
{
9193
type: 'propertyEditorDataSource',
92-
dataSourceType: 'picker',
94+
dataSourceType: UMB_PICKER_DATA_SOURCE_TYPE,
9395
alias: 'Umb.PropertyEditorDataSource.UserPicker',
9496
name: 'User Picker Data Source',
9597
api: () => import('./example-user-picker-data-source.js'),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const UMB_PICKER_DATA_SOURCE_TYPE = 'Umb.DataSourceType.Picker';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './collection-data-source/is-picker-collection-data-source.guard.js';
2+
export * from './constant.js';
23
export * from './searchable-data-source/is-picker-searchable.data-source.guard.js';
34
export * from './tree-data-source/is-picker-tree-data-source.guard.js';
45
export type * from './types.js';

src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/entry-point.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { manifests as entityDataPickerManifests } from './manifests.js';
22
import type { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
3+
import { UMB_PICKER_DATA_SOURCE_TYPE } from '@umbraco-cms/backoffice/picker-data-source';
34
import type { ManifestPropertyEditorDataSource } from '@umbraco-cms/backoffice/property-editor-data-source';
45

56
export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
@@ -11,7 +12,7 @@ export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
1112
extensionRegistry
1213
.byTypeAndFilter<'propertyEditorDataSource', ManifestPropertyEditorDataSource>(
1314
'propertyEditorDataSource',
14-
(manifest) => manifest.dataSourceType === 'picker',
15+
(manifest) => manifest.dataSourceType === UMB_PICKER_DATA_SOURCE_TYPE,
1516
)
1617
.subscribe((pickerPropertyEditorDataSource) => {
1718
if (pickerPropertyEditorDataSource.length > 0 && !initialized) {

0 commit comments

Comments
 (0)