MGNLUI-4519 - Getting issue details... STATUS
Goal
- should work with modernised form framework
- should support the current select field functionality
- single select with pre-defined options
- single select with items coming from the back-end (JCR or different)
- multi-selects covering the same functionality as single selects
- design a new definition hierarchy that would be clearer than the current (e.g. the option of connecting to backends is provided in a hacky and restrictive way aka 'remotePath' or smth')
Definition proposal
Items can originate from
- Preconfigured or inline options, i.e. within the select definition itself
- a "remote" JCR location (e.g. a workspace other than config)
- an actual remote data source, e.g. a REST based service
Below it's an example of the two latter definitions
select:
label: select
class: info.magnolia.ui.framework.databinding.definition.JcrSingleSelectFieldDefinition
datasource:
class: info.magnolia.ui.datasource.jcr.JcrDatasourceDefinition
workspace: contacts
sortable: true #true it's the default value actually
sortByProperties: # if omitted will default to describeByProperty, if the latter is present
- foo
- bar
describeByProperty: bar
selectRest:
label: selectRest
class: info.magnolia.ui.framework.databinding.definition.RestSingleSelectFieldDefinition
datasource:
class: info.magnolia.restcontentapp.rest.RestDataProviderDefinition
url: https://restcountries.eu/rest/v2/regionalbloc/eu
jsonPathPredicate: "$.[*].name"
Below an example of inline or preconfigured options: it still uses a datasource but it's a "static" or fixed-size one (naming still uncertain)
Preconfigured options are by default sorted alphabetically in ascending order, unless sortOptions is set to false
select:
label: select
class: info.magnolia.ui.framework.databinding.definition.PreconfiguredSingleSelectFieldDefinition
textInputAllowed: true
filteringMode: startsWith
datasource:
class: info.magnolia.ui.framework.datasource.impl.FixedSizeDatasourceDefinition
properties:
- name: foo
value: Foo
- name: bar
value: Bar
- name: qux
value: Qux
A BaseSelectFieldDefinition will be shared by selection components with single or multiple selection capabilities
public class BaseSelectFieldDefinition<T, DS> extends ConfiguredFieldDefinition<T> {
...
/**
* An object representing the DataProvider configuration definition.
*/
private DS datasource;
The data provider is obtained through DatasourceSupport at AbstractSelectFieldFactory
public abstract class AbstractSelectFieldFactory<DS, D extends BaseSelectFieldDefinition<T, DS>, T, F extends Component & HasValue<T>> extends AbstractFieldFactory<D, T, F> {
...
@Inject
public AbstractSelectFieldFactory(ComponentProvider componentProvider, D definition, ..., DatasourceSupport datasourceSupport) {
super(definition, componentProvider, locale, i18NAuthoringSupport);
this.dataSourceBundle = datasourceSupport.getDatasourceBundle(getDefinition().getDatasource());
}
...
protected Optional<? extends IconResolver> getIconResolver() {
return dataSourceBundle.lookup(IconResolver.class, getDefinition().getDatasource());
}
protected Optional<? extends ItemDescriber> getItemDescriber() {
return dataSourceBundle.lookup(ItemDescriber.class, getDefinition().getDatasource());
}
protected Optional<? extends DataProvider> getDataProvider() {
return dataSourceBundle.lookup(DataProvider.class, getDefinition().getDatasource());
}
Concrete factories implementing this will do something like the following
getDataProvider().ifPresent(dp -> select.setDataProvider(dp)); getItemDescriber().ifPresent(describer -> select.setItemCaptionGenerator(item -> describer.describe(item))); ...
Converter resolving strategy
We need to come up with a general strategy as to how to provide converters, that is the Vaadin mechanism responsible for translating between model (data type used by the backend) and presentation (data type used by the UI) cause the two may not match (e.g. a TextField displaying a Number, where the presentation type is a String and the model some numeric type).
In M5 the logic setting a converter and creating a default value is bound to the component itself (see https://git.magnolia-cms.com/projects/PLATFORM/repos/ui/browse/magnolia-ui-form/src/main/java/info/magnolia/ui/form/field/factory/AbstractFieldFactory.java)
With Vaadin 8 this concern has moved to a separate Binder class. A special case of converter in Magnolia is IdentifierToPathConverter which is used e.g. to convert an id in the form workspace:uuid into a corresponding path. This used to resolve e.g. assets in dam or to select items in the UI with a chooser dialog.
Since we want to be JCR-agnostic we'd like our definitions to be free from JCR specific terms, such as workspace. Also, since we now have introduced datasource in select field definitions, it would be nice to reuse such feature without additional configuration.
Proposal
Data source implementations know how to resolve complex items (such as a JCR Node) into simpler objects (a Node's String identifier for instance) back and forth. Data source bundles will eventually register such converters. For instance
/**
* JCR-specific implementation of {@link Converter}. Turns a Node (presentation type) into its identifier (model type or a String) and vice versa.
*/
public class JcrItemToLinkConverter implements Converter<Node, String> {
...
public JcrItemToLinkConverter(JcrDatasourceDefinition definition, Provider<Context> contextProvider) {
this.definition = definition;
this.contextProvider = contextProvider;
}
@Override
public Result<String> convertToModel(Node value, ValueContext context) {
if (value == null) {
return Result.ok(null);
}
return Result.ok(value.getIdentifier());
}
@Override
public Node convertToPresentation(String id, ValueContext context) {
if (id == null) {
return null;
}
try {
return new LazyNodeWrapper(getSession().getNodeByIdentifier(id));
} catch (RepositoryException e) {
log.warn("Could not get a JCR node by identifier " + id, e);
return null;
}
}
...
After registration, the new Converter will be available via DataSourceSupport
public JcrDataSourceBundle(Provider<Context> contextProvider) {
super(JcrDatasourceDefinition.class);
...
register(Converter.class, def -> new JcrItemToLinkConverter(def, contextProvider));
}
A new interface ItemToLinkConverterResolverStrategy will be introduced and injected into ConfiguredBinder. The converter resolver strategy will take care of resolving, if possible, the appropriate converter.
/**
* Attempts to resolve an item to link {@link com.vaadin.data.Converter} for a given field based on its datasource configuration.
* Typically item to link converters are used to represent a complex object as a simple object that references the complex one.
* For instance, a JCR Node may be represented by its identifier, that is a plain String.
* In this example, an item to link converter will be able to transform a Node to its identifier and vice versa.
*
* @see DefaultItemToLinkConverterResolverStrategy
* @see MultiItemToLinkConverter
*/
public interface ItemToLinkConverterResolverStrategy<PRESENTATION, MODEL> {
/**
* @return the appropriate item to link converter based on a field's datasource configuration.
*/
Optional<? extends Converter<PRESENTATION, MODEL>> resolveFromDatasource(Object datasource);
}
A default implementation may look like this
public class DefaultItemToLinkConverterResolverStrategy implements ItemToLinkConverterResolverStrategy {
...
@Inject
public DefaultItemToLinkConverterResolverStrategy(DatasourceSupport datasourceSupport) {
this.datasourceSupport = datasourceSupport;
}
@Override
public Optional<? extends Converter> resolveFromDatasource(Object datasource) {
if (datasource == null) {
return Optional.empty();
}
return datasourceSupport.getDatasourceBundle(datasource).lookup(Converter.class, datasource);
}
}
Finally ConfiguredBinder will bind the converter to a field based on the latter's datasource (or converterClass) configuration.
Git PR
https://git.magnolia-cms.com/projects/PLATFORM/repos/ui/pull-requests/624/overview