major
#29896
Typed configuration: config:ref addresses an entry of a keyed list by a globally unique key without naming the path to it; a view overlay addresses every element by its id
A module extends a view of a module it depends on with a same-path overlay (WEB-INF/views/<same path>.view.xml). Until now the overlay had to repeat the whole path from the root of the view to the element it changes - every ancestor, written in the nesting of the base - and the lists on the way had to be keyed, which the lists of split panes and switch cases were not. A deep extension point meant a long fragment coupled to every layer in between; moving the target inside the base broke every fragment.
Found in an application (E-Home, 8.0.0-alpha10): an optional fragment module (development tooling) adds a tab "Virtual device" to the device detail of the app, a tab bar inside split panes and switch cases of devices.view.xml. Workaround at the time: the tab bar was extracted into a view of its own (projects/device-tabs.view.xml) which the fragment overlays. The engine's own user-menu.view.xml promised the same kind of extension and could not keep it: its menu sits inside <switch><default>, so a contributed entry appended a second menu.
Solution
Referencing elements in typed configuration (com.top_logic.basic). An item with a property annotated @GlobalKey is addressed from anywhere above it by an element carrying config:ref="<key>" (ConfigurationSchemaConstants.CONFIG_REF), without naming the path to it:
<view xmlns:config="http://www.top-logic.com/ns/config/6.0"> <tab-bar config:ref="device-tabs"> <tabs> <tab id="virtual-device" config:position="after" config:reference="general"> ... </tab> </tabs> </tab-bar> </view>
- The value of a @GlobalKey property identifies the item that carries it in the whole configuration the reader produces from its sources - the base and all overlays together - whether the item is an entry of a list (keyed by that property, by another one, or not at all), the value of an item property or an entry of a map. The reader rejects two items with the same value (GlobalKeys.checkUnique, run after every source that contains such items). The property typically also serves as @Key of the lists holding such items. An item without value is anonymous as before: it takes part in no uniqueness check and cannot be referenced.
- A referencing element stands in the content of an element and is no entry of a property of that element. The reader resolves the key among all items with a global key below the enclosing element, in the configuration as read so far (ConfigurationReader.readReferencingElement, GlobalKeys.visitItems; the default-container list of the enclosing element is taken as read so far, not as stored), and updates the item found as an update by key does: its properties are merged with the element's contents, config:override="true" replaces it, config:operation="remove" removes it, config:position / config:reference move a list entry within its list. The item keeps its key: the element may repeat it, but not change it. The tag of the referencing element determines the configuration type, as the tag of an entry of the holding property does (the property name for a plain item property).
- The enclosing element bounds the search: the root element, an element written by its path, or another referencing element. Referencing elements therefore nest the way the base nests, with the layers in between left out, and the nesting is checked - a reference to an entry outside the enclosing element fails.
- Errors: a key the enclosing element does not contain, config:ref on the root element or on an entry of a list property (<tabs>, <panes>, ...), a combination with config:index or config:operation="add", positioning an item that is no list entry, a different key value written on the element, two items with the same global key.
Every element of a view is addressable by its id (com.top_logic.layout.view). UIElement.Config gets the optional property id; the panes of a <split-panel>, the cases of a <switch> and the step sources of a <wizard> get an id as well, and every list of elements (the children of a panel or stack, the content of a case, a pane, a <visible-if>, a tab, the columns of <columns>, the tiles of a dashboard, the slots of the app bar, ...) is keyed by it (List<UIElement.Config>, @Key(UIElement.Config.ID)). The id of an element and of a tab or section, a pane, a case, a sidebar item, a bottom-bar item and a menu group is a @GlobalKey: unique in the whole view, base and overlays together (a <view-ref> embeds a view with ids of its own), and the address an overlay uses. A wizard step's id stays unique within its wizard only: it is the value of the step channel, and two wizards of one view may share step names. The id of a <tab> and of an accordion <section> is mandatory: it is the route segment, the reveal path and the personalization key of the part. Sidebar separators stay anonymous. The audit TestElementListsKeyed checks that every element list is keyed by a global id and every part id is global.
The path form stays as the underlying merge: an overlay may still write the path to an element, naming each element on the way by its id, and that is how an entry of a list property (<tab> in <tabs>, <channel> in <channels>) is matched - by key, with config:position, config:override, config:operation. A referencing element inside an element written by its path addresses an element below that one.
Anonymous entries in keyed lists (com.top_logic.basic): an entry of a keyed list whose key is not set (null or empty, ConfigUtil.isAnonymousKey) is anonymous. It is appended in order, matched by nothing, and any number of anonymous entries may occur in one list; an update, a removal or a config:reference without a key is an error. Elements of a view therefore need an id only where an overlay is meant to address them. (Before, a keyed list allowed at most one entry without a key per source, and an anonymous overlay entry merged into the base's anonymous entry.) The form editors (DefaultValueModel, EditorFactory) and the configuration editor (tl-layout-configedit) follow the rule: a duplicate-key check skips anonymous entries, a pending entry of a list with an optional key is committed without a key, and the view designer generates a key only where the key is mandatory (tabs, sections).
Addressing by position (config:index, ConfigurationSchemaConstants.LIST_INDEX): for a base that gives no ids, an overlay entry carrying config:index="n" addresses the entry at zero-based position n` of the list as merged so far and updates it (replaces it with `config:override="true", moves it with config:position); an index out of range, a negative or non-numeric value and a combination with config:operation="add" or "remove" or with config:ref are errors. The entry may set an id at the same time, which names the anonymous base element for further overlays. The index is not stable against changes of the base and is the emergency path where no id was given.
Shipped views. The views written for overlays name their extension points, and every shipped overlay addresses its target with config:ref:
- app.view.xml: <sidebar id="sidebar">, <app-bar id="app-bar">; the trailing elements dev-menu, user-menu to position against. The React demo's overlay writes <sidebar config:ref="sidebar"> and <app-bar config:ref="app-bar">.
- user-menu.view.xml: switch session, case anonymous, menu account-menu (renamed from account, which is the id of its group) - the demo contributes its group through <menu config:ref="account-menu">.
- dev-menu.view.xml: menu dev-menu with the groups page, automation, reference; tl-layout-view-agent contributes through <group config:ref="automation">.
- settings-tabs.view.xml: tab bar settings-tabs; tl-layout-view-contact adds its tab through it.
- admin/admin.view.xml: tab bar sections, inner tab bars access-control-views, monitoring-views, system-views (renamed from views, which the three sections shared); tl-dev-tools, tl-model-search-react and tl-layout-view-agent add their sections through <tab-bar config:ref="sections">. The views of the sections name their panes, panels, tables and forms (main, list, detail, list-panel, detail-panel, table, form, the coverage view coverage, findings, rules, the log view files, lines, ...), so an application adds a command to a toolbar or a field to a form.
The article view-layer/overlays describes same-path overlays with the referencing element first, then the merge rules and the path form, the engine's examples and how to write a view others extend; view-layer/app-shell follows.
Tests. TestConfigReference (basic: references below the root, nested, inside an element on the path, into lists keyed otherwise, item properties and maps, entries read before in the same source, override, remove, move, every error case, uniqueness of global keys across property kinds), TestKeyedListAnonymousEntries (anonymous entries, position, index), TestDefaultValueModel, TestConfigChildren/`TestConfigPendingEntries` (editors), TestViewOverlayKeyedLists (the nested split-panel/switch/tab-bar merge by reference and by path, both producing the same view, nested references, a reference into the kind-keyed content slot of the shell and to the content element of the view, override, remove, move, the containment check, the uniqueness of ids across lists, index, written merge) and the audit TestElementListsKeyed.
Not part of this ticket: the classic layout XML, and <slot-content> (projecting content into a declared slot at runtime), which addressable lists do not replace.
Migration
- An id is unique in the whole view, not only among siblings: a view with two elements, tabs, panes, cases, sidebar items or menu groups of the same id is rejected when it is loaded. In the shipped views, the inner tab bars of the admin sections are access-control-views, monitoring-views, system-views (before: views in every section) and the menu of the user menu is account-menu (before: account, the id its group keeps). An overlay that reached those elements by their old ids addresses them with config:ref now: <tab-bar config:ref="system-views">, <menu config:ref="account-menu">.
- A <tab> of a <tab-bar> and a <section> of an <accordion> in a .view.xml must carry an id; a view with a tab or section without id is rejected when it is loaded.
- An overlay that contributed an anonymous entry (no key) to a keyed list of a view and relied on it merging into the base's anonymous entry now appends a second entry; give the base entry an id and address it.