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