Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ public class ElementPickerConfiguration : IIgnoreUserStartNodesConfig
/// <inheritdoc />
[ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)]
public bool IgnoreUserStartNodes { get; set; }

/// <summary>
/// Gets or sets the content type filter for allowed selections.
/// </summary>
[ConfigurationField("allowedContentTypes")]
public string? AllowedContentTypeIds { get; set; }
}
121 changes: 118 additions & 3 deletions src/Umbraco.Core/PropertyEditors/ElementPickerPropertyEditor.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
using Umbraco.Cms.Core.IO;
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.PropertyEditors;

Expand Down Expand Up @@ -39,9 +45,17 @@
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
DataEditorAttribute attribute,
Comment thread
NguyenThuyLan marked this conversation as resolved.
ILocalizedTextService localizedTextService,
IElementService elementService,
ICoreScopeProvider coreScopeProvider)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
=> _jsonSerializer = jsonSerializer;
{
_jsonSerializer = jsonSerializer;
Validators.Add(new ElementPickerValidatorRunner(
jsonSerializer,
new AllowedTypeValidator(localizedTextService, elementService, coreScopeProvider)));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The media picker equivalent also has MinMaxValidator and StartNodeValidator. If the element picker supports those configurations, then we should also server-side validate them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, Element Picker doesn't have min/max configurations. Is it possible to add this option to Element Picker?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think they are worth adding. In general we should look to align the document (content), media and element pickers.

}

Check warning on line 58 in src/Umbraco.Core/PropertyEditors/ElementPickerPropertyEditor.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Constructor Over-Injection

ElementPickerPropertyValueEditor has 7 arguments, max arguments = 5. This constructor has too many arguments, indicating an object with low cohesion or missing function argument abstraction. Avoid adding more arguments.

public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
Expand All @@ -63,4 +77,105 @@
}
}
}

internal sealed class ElementPickerValidatorRunner : IValueValidator
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
{
private readonly AllowedTypeValidator _validator;

public ElementPickerValidatorRunner(IJsonSerializer jsonSerializer, AllowedTypeValidator validator)
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
=> _validator = validator;

public IEnumerable<ValidationResult> Validate(
object? value,
string? valueType,
object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
if (dataTypeConfiguration is not ElementPickerConfiguration configuration)
{
return [];
}

Guid[]? guids = value is IEnumerable<string> strings
? strings.Select(s => Guid.TryParse(s, out Guid g) ? g : (Guid?)null).Where(g => g.HasValue).Select(g => g!.Value).ToArray()
: null;

return _validator.Validate(guids, configuration, valueType, validationContext);
}
}

internal sealed class AllowedTypeValidator : ITypedJsonValidator<Guid[], ElementPickerConfiguration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IElementService _elementService;
private readonly ICoreScopeProvider _coreScopeProvider;

public AllowedTypeValidator(
ILocalizedTextService localizedTextService,
IElementService elementService,
ICoreScopeProvider coreScopeProvider)
{
_localizedTextService = localizedTextService;
_elementService = elementService;
_coreScopeProvider = coreScopeProvider;
}

public IEnumerable<ValidationResult> Validate(
Guid[]? value,
ElementPickerConfiguration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
if (value is null || value.Length == 0 || configuration is null)

Check warning on line 129 in src/Umbraco.Core/PropertyEditors/ElementPickerPropertyEditor.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Conditional

Validate has 1 complex conditionals with 2 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
{
return [];
}

HashSet<Guid> allowedContentTypeKeys = ParseAllowedContentTypeKeys(configuration.AllowedContentTypeIds);

// No filter configured — all element types are allowed.
if (allowedContentTypeKeys.Count == 0)
{
return [];
}

using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
IElement[] elements = _elementService.GetByIds(value).ToArray();
Comment thread
NguyenThuyLan marked this conversation as resolved.
scope.Complete();

foreach (IElement element in elements)
{
if (allowedContentTypeKeys.Contains(element.ContentType.Key) is false)
{
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidObjectType"),
["value"])
];
}
}

return [];
}

private static HashSet<Guid> ParseAllowedContentTypeKeys(string? configValue)
{
if (configValue.IsNullOrWhiteSpace())
{
return [];
}

var result = new HashSet<Guid>();
foreach (var entry in configValue.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries))
{
if (Guid.TryParse(entry, out Guid guid))
{
result.Add(guid);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { UmbElementTreePickerDataSource } from '../picker-data-source/element-tree.picker-data-source.js';
//import { UMB_ELEMENT_ENTITY_TYPE } from '../entity.js';
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
import { html, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
//import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models';
import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree';

@customElement('umb-input-element')
Expand All @@ -16,6 +18,9 @@ export class UmbInputElementElement extends UmbFormControlMixin<string | undefin
@property({ type: Boolean })
folderOnly = false;

@property({ type: String })
allowedContentTypeIds?: string;

@property({ type: Number })
min = 0;

Expand Down Expand Up @@ -68,6 +73,7 @@ export class UmbInputElementElement extends UmbFormControlMixin<string | undefin
const dataSourceConfig = [
{ alias: 'folderOnly', value: this.folderOnly },
{ alias: 'startNode', value: this.startNode },
{ alias: 'allowedContentTypeIds', value: this.allowedContentTypeIds },
];
return html`
<umb-input-entity-data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { UmbElementFolderItemRepository } from '../folder/repository/item/elemen
import { UmbElementItemRepository } from '../item/repository/element-item.repository.js';
import { UmbElementTreeRepository } from '../tree/element-tree.repository.js';
import type { UmbElementTreeChildrenOfRequestArgs, UmbElementTreeRootItemsRequestArgs } from '../tree/types.js';
import { getConfigValue } from '@umbraco-cms/backoffice/utils';
import type { UmbElementTreeItemModel } from '../tree/types.js';
import { getConfigValue, splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbElementFolderItemDataResolver } from '../folder/data-resolver/element-folder-item-data-resolver.js';
import { UmbElementItemDataResolver } from '../item/data-resolver/element-item-data-resolver.js';
Expand All @@ -20,6 +21,7 @@ import type { UmbItemDataResolver } from '@umbraco-cms/backoffice/entity-item';
import type { UmbPickerTreeDataSource } from '@umbraco-cms/backoffice/picker-data-source';

export class UmbElementTreePickerDataSource extends UmbControllerBase implements UmbPickerTreeDataSource {
#allowedContentTypeIds?: Array<string>;
#dataType?: { unique: string };
#elementItem = new UmbElementItemRepository(this);
#folderItem = new UmbElementFolderItemRepository(this);
Expand All @@ -44,6 +46,8 @@ export class UmbElementTreePickerDataSource extends UmbControllerBase implements
setConfig(config: UmbConfigCollectionModel | undefined) {
this.#folderOnly = Boolean(getConfigValue(config, 'folderOnly'));
this.#startNode = getConfigValue(config, 'startNode');
const allowedIds = getConfigValue(config, 'allowedContentTypeIds');
this.#allowedContentTypeIds = allowedIds ? splitStringToArray(allowedIds) : undefined;
}

async requestTreeStartNode() {
Expand Down Expand Up @@ -80,5 +84,14 @@ export class UmbElementTreePickerDataSource extends UmbControllerBase implements
return this.#folderOnly ? this.#folderItem.requestItems(uniques) : this.#elementItem.requestItems(uniques);
}

treePickableFilter = (treeItem: UmbTreeItemModel): boolean => treeItem.isFolder === this.#folderOnly;
treePickableFilter = (treeItem: UmbTreeItemModel): boolean => {
if (treeItem.isFolder !== this.#folderOnly) return false;

if (!this.#folderOnly && this.#allowedContentTypeIds?.length) {
const elementItem = treeItem as UmbElementTreeItemModel;
return this.#allowedContentTypeIds.includes(elementItem.documentType.unique);
}

return true;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';

export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.ElementPicker.AllowedElementTypes',
name: 'Element Picker Allowed Element Types Property Editor UI',
element: () =>
import('./property-editor-ui-element-picker-allowed-element-types.element.js'),
meta: {
label: 'Element Picker Allowed Element Types',
icon: 'icon-plugin',
group: '#propertyEditorUIGroups_pickers',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';

import '@umbraco-cms/backoffice/document-type';

@customElement('umb-property-editor-ui-element-picker-allowed-element-types')
export class UmbPropertyEditorUIElementPickerAllowedElementTypesElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
@property()
public set value(value: string) {
this.#selection = value ? value.split(',') : [];
}
public get value(): string {
return this.#selection.join(',');
}

#selection: Array<string> = [];

#onChange(event: CustomEvent & { target: { selection: string[] } }) {
this.value = event.target.selection.join(',');
this.dispatchEvent(new UmbChangeEvent());
}

override render() {
return html`<umb-input-document-type
.elementTypesOnly=${true}
.selection=${this.#selection}
@change=${this.#onChange}></umb-input-document-type>`;
}
}

export default UmbPropertyEditorUIElementPickerAllowedElementTypesElement;

declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-element-picker-allowed-element-types': UmbPropertyEditorUIElementPickerAllowedElementTypesElement;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export class UmbElementPickerPropertyEditorUIElement
this._startNode = startNodeId.length
? { unique: startNodeId[0], entityType: UMB_ELEMENT_FOLDER_ENTITY_TYPE }
: undefined;

this._allowedContentTypes = config.getValueByAlias('allowedContentTypes');
}

@state()
Expand All @@ -60,6 +62,9 @@ export class UmbElementPickerPropertyEditorUIElement
@state()
private _startNode?: UmbTreeStartNode;

@state()
private _allowedContentTypes?: string;

override focus() {
return this.shadowRoot?.querySelector('umb-input-element')?.focus();
}
Expand Down Expand Up @@ -91,6 +96,7 @@ export class UmbElementPickerPropertyEditorUIElement
.minMessage=${this._minMessage}
.max=${this._max}
.maxMessage=${this._maxMessage}
.allowedContentTypeIds=${this._allowedContentTypes}
?folderOnly=${this._folderOnly}
?readonly=${this.readonly}
@change=${this.#onChange}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { manifest as schemaManifest } from './Umbraco.ElementPicker.js';
import { manifest as allowedElementTypesManifest } from './config/allowed-element-types/manifests.js';
import { manifest as schemaManifest } from './Umbraco.ElementPicker.js';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';

const propertyEditorUi: ManifestPropertyEditorUi = {
Expand Down Expand Up @@ -32,9 +33,16 @@ const propertyEditorUi: ManifestPropertyEditorUi = {
],
weight: 110,
},
{
alias: 'allowedContentTypes',
label: 'Allow items of type',
description: 'Select the applicable element types',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.ElementPicker.AllowedElementTypes',
weight: 120,
},
],
},
},
};

export const manifests: Array<UmbExtensionManifest> = [propertyEditorUi, schemaManifest];
export const manifests: Array<UmbExtensionManifest> = [propertyEditorUi, allowedElementTypesManifest, schemaManifest];
Loading
Loading