Goal
An application built on the com.top_logic.layout.view layer has to assemble its app-bar account area by hand: a <derived-channel expr="currentUser()">, an <avatar> in the bar's <children>, and one flat placement="TOOLBAR" button per account action. The Consulting application does exactly that, and props it up with CSS that selects the buttons by their icon glyph because no per-command styling reaches the DOM.
The account area is offered here as a core view an application references, and extends through a same-path overlay - the way the administration view is already offered. What a user changes with a single decision is an entry of that menu; what wants a form is a dialog the menu opens. Everything found along the way is covered here.
A user menu, composed rather than built
An earlier attempt (ticket #29108) added a bespoke UserMenuElement with its own UserMenuControl and TLUserMenu React component, and was withdrawn in the same ticket because it reimplemented forms and buttons that the view layer already composes. The concept returns as a composition of existing parts:
- WEB-INF/views/user-menu.view.xml in tl-layout-view, declaring its own currentUser channel, so an application adds the menu with a single <view-ref view="user-menu.view.xml"/> in the app bar and wires nothing.
- An application adds, repositions or removes entries with a same-path overlay merged by the ViewLoader in dependency order, as tl-model-search-react already extends admin/admin.view.xml. The core module needs no knowledge of the contributor.
Its entries are Login (anonymous only), Change password, Settings and Logout (authenticated only), plus one per language and one per theme.
A generic menu carrier
ContextMenuElement already turns the commands declared inside it into a popup, filters them by visibility, groups them by clique and dispatches the selection - but only through a contextmenu event over the content it wraps. The single missing piece is a menu opened by clicking a trigger.
Rather than a second implementation, the contribution-building part of ContextMenuElement moved into AbstractMenuElement, and ContextMenuRegionControl together with TLContextMenuRegion became MenuRegionControl / TLMenuRegion, parameterized by a MenuTrigger: contextmenu places the menu at the pointer, click anchors it below the region's bounding box. Both report the viewport coordinates the menu appears at, so ContextMenuOpener needed no change.
The new <menu> element takes its trigger from its children - the same content property <context-menu> uses for its right-click region - so the two elements have one shape and differ only in the gesture. Every command declared inside a <menu> becomes an entry whatever its CommandPlacement: a command written inside a menu is an entry of that menu by construction, and CommandPlacement therefore needs no new value.
A menu builds one contribution per group of entries rather than one flat list, because ContextMenuOpener separates the entries of one contribution from those of the next. The commands written in the view are one group and each command source another, so the account actions, the languages and the themes are divided by separators, and a group with nothing to offer drops out together with its separator.
Commands derived at runtime
Feeding a menu from state rather than from configuration needed a seam: ViewCommandSource is a configured source of commands whose number and identity are known only at runtime, contributed through a menu's <command-sources> property and appended after the declared commands. Two of them are offered.
<theme-commands> contributes one entry per theme configured in UIThemeService, labelled and iconed by the theme itself, with the active one offered as disabled - which is both what it means and how the menu shows which theme is in effect. UITheme gains an icon property, since the theme is what knows how it looks, and SetThemeCommand exposes applyTheme so the source reuses the activation instead of copying it. A single SetThemeCommand cannot do this: it names one static theme id, so a view using it must spell out every theme it knows and can never tell which is active.
<language-commands> contributes one entry per supported locale of the ResourcesModule, labelled with the language's own name in that language - "Deutsch", "English" - which is what a reader looking for their language recognises and needs no translation of its own. Unlike a theme, a language cannot be applied to a rendered page, so the switch stores it on the account, tells the session and reloads. Neither source offers anything while there is only one choice, and the language source offers nothing to an anonymous session, which has no account to store a choice on.
A password is changed the same way from either side
ChangePasswordApplyAction read the account to change from LoginAction#ACCOUNT_CHANNEL and always ended by calling LoginAction.proceedAfterPassword, so change-password.view.xml could only continue a login. The coupling was incidental: the classic ChangePasswordComponent changes the password of a Person without any login in progress.
The account now falls back to the session's own when no login is waiting, and only a pending login - which is what the presence of that channel means - continues afterwards. change-password.view.xml shows its "your password has expired" hint only on that path, through a <switch> over the account channel, and is therefore one dialog serving both.
Personal settings
WEB-INF/views/settings.view.xml is a form over the account carrying what a single menu entry cannot express: the timezone and the country. It is over tl.accounts:Person, so the view module needs nothing of the Contacts model; a module that has it - or the application - contributes the profile fields through a same-path overlay, which tl-layout-view-contact does.
A user also sets their own second factor up here and drops it again, in the same enrolment dialog the login flow uses. The account keeps its requirement, since a factor the user chose is one they may drop again; each button is offered only in the state where it means something, decided by whether a secret is stored.
The account area closes the app bar
The account area was placed among the bar's inline children, which put it right of the title and left of everything else. Where it belongs is the other end, past the buttons of toolbar-placed commands - and the bar had no way to say so: its actions area is built from commands, and its leading area is the wrong end.
An app bar now takes a <trailing> region beside its <leading> one, and both compose the same way, because an app bar is horizontal and several elements at one of its ends stand side by side rather than stacked, which is what the generic content combination would do with them. An actions area ahead of the trailing one has already claimed the free space, so the trailing one does not claim it a second time and strand the command buttons in the middle of the bar.
One button bar in the settings dialog
The account tab wrapped its form in a <panel> to keep Save out of the other tabs, which gave that tab a button bar of its own beside the dialog's - Save in one, Close in the other - and the profile tab of tl-layout-view-contact did the same. A form contributes its commands to the enclosing scope only for as long as it is displayed, and a tab that is not the active one is not, so the panels are gone and Save joins Close in the single bar the dialog already has.
That holds only if the form exists when the bar is built. ReactTabBarControl created its active tab's content at the first write, by which time the enclosing button bar had been rendered without it: Save appeared only after leaving the tab and coming back. The content is now created when the tab bar attaches, ahead of the render, where the attach propagation that follows the hook reaches it.
The security tab lists buttons rather than fields, so nothing brought along the padding a form carries by itself and the buttons glued to the dialog border. An <inset> gives the list the same page inset, so both tabs hold their content at the same distance from it.
Leaving the session stands apart in the account menu
A menu draws a separator between the groups of entries it is composed of, and everything a view wrote was one group: the entry for leaving sat directly above the entries for settling in. A menu now takes further groups after the entries written in <commands>, through a <groups> property, each set off from the one before it.
This is the mechanism the separator already belongs to rather than an entry that draws a line: a group none of whose entries are currently available is left out together with its separator, so the account menu of an anonymous session - which offers Login and nothing else from the first group - still opens without a line above or below it. The account menu puts entering and leaving in the first group and what the account carries in the second; the two directions are one group because exactly one of them is ever offered.
A language and a country under their flag
The languages of the account menu were told apart by their own names alone, and a country field was 249 lines of text. Each now carries a flag.
The images come from the flag-icons stylesheet, which a webjar supplies and which offers one per ISO 3166-1 country code and a handful beyond it: the flag of a region, of a union, and of a language area that is no country. A flag is therefore an ordinary CSS-class icon and needs no plumbing of its own.
A country is an ISO 3166-1 code, so its flag needs no convention - only a resource provider saying so. The type had none registered at all and fell through to the default one, whose label for a country is the debugging form of the object; the contact module made up for that with a provider whose whole content was to label a country by its name. That labelling is what com.top_logic.util.CountryResourceProvider now does beside the type, so one provider answers for the label and the flag together.
Which flag stands for a language is a convention rather than a fact, so it is configuration: LanguageFlags states it for the well-known language tags - the flag of the country the language is most widely associated with, of the area it is spoken across where no single country carries it, and the fallback flag where neither applies. An application that disagrees overrides the entry it disagrees with. Entries are consulted most specific first, so a region carries its own flag only where the application supports that region as a language in its own right. A language no entry covers is given the fallback flag rather than nothing: a list in which some entries carry a flag and others do not reads worse than one in which they all do.
Both live beside the country type rather than in the view layer, so what presents a language or a country anywhere can name its flag.
A session begins where its own user begins
Logging in left the session on whatever page had been reached before it. The redirect that carries out the session swap points back at the URL the request came in on, and a URL naming a page asks for that page - so the start page of the user logging in never applied, because someone else's navigation had already answered the question for them.
A session that has just replaced another one is marked as such, and its first view request drops the route the URL still carries. The page then comes from the start page of whoever took the session over, or from the application's default when they have chosen none. Logging out is the same swap in the other direction and is treated the same way.
Defect: an option of a drop-down could not carry an image
A drop-down asked its label provider for an option's image and dropped it unless that provider happened to be a ResourceProvider - which the one it is handed never is. The client has carried the image of an option all along, in the option rows, the chips and the read-only values alike, and none of it could ever be reached: a select is built with the meta label provider, so the branch writing the image was dead for every drop-down in the application. The provider handed in is now the resource one, which answers for an image as well as for a label; the two registries of LabelProviderService fall back to each other, so a type registered only for its label is labelled exactly as before.
Two things the image then ran into, both of them the encoded form reaching a reader that could not read it. The image was encoded without being resolved, unlike the one of a button, so a reference - the icon a theme configures for a type - arrived as the reference, which the client has no theme to look up, and the whole encoded string became a CSS class: one containing a colon, naming nothing. And the invisible image encodes as a form of its own, which became an element carrying a class that draws nothing and still occupies the width of an icon, so every option of an enumeration was indented by an icon it does not have. It is now left out of the descriptor altogether, and both of the client's decoders answer it with nothing rather than with an element - it belongs to the vocabulary they decode, and one of them documented it while falling through to its CSS-class fallback. The drop-down also reads an option's image through the decoder the buttons, the menus and the sidebar use, rather than through the one that knew two of the prefixes.
Defect: per-command presentation does not reach the button
ViewCommand.Config declares css-classes, documented as additional CSS classes to apply to the command's UI element. It parses, it survives instantiation, and ViewCommandModel exposes it - and nothing reads it. The React CommandModel that ReactButtonControl serializes has no member for it, so no state key is written and the client has nothing to apply. A view author styling a command through the property gets no error, no warning and no effect. Reported against 8.0.0-alpha7 by the Consulting application, whose finding mark destructive actions better is exactly what the property is for; it works around the gap with
{{{#!css .tlReactButton:has(> .bi-trash) { color: var(--text-error); } }}}
a convention held by hand, which silently stops covering a delete button given a different icon and silently starts covering a harmless command given the trash one.
display is dead on the same path, for a narrower reason. ToolbarBuilder honours CommandModel#getDisplayMode(), so display="icon-only" works on a panel toolbar; the app bar builds its action buttons itself in AppBarElement, straight through new ReactButtonControl(context, model), and is therefore the path on which it is dropped.
Both are one omission: the constructor that wires a button from a CommandModel syncs label, executability, visibility, image, tooltip and key gesture, but neither of the two remaining presentation properties. The fix belongs there rather than in the app bar, so every call site is covered at once. A container now contributes only its default, through ReactButtonControl#setDefaultDisplayMode, which applies to a command requesting no mode of its own - keeping the icon-only-without-image guard in one place and reporting it once, as an author error about that command rather than about a container default that happens not to fit it. A MenuEntry carries the classes too, so the property behaves the same way on a command rendered as a menu entry.
Defect: a menu entry rendered no icon
TLMenu spliced an entry's icon into a class attribute in the encoded form that ContextMenuOpener produces. For css:bi bi-sun that yields the two classes css:bi and bi-sun, dropping the icon font's own base class, so no icon rendered; a /icons/... or theme:... entry rendered nothing either. Menu entries now go through ThemeIcon, which decodes the form, as buttons already did. Every context menu in the application was affected, not only the new one.
Defect: text on the accent app bar was unreadable
A text element brings the primary text colour along, which is near-black, and the accent bar it sits on is dark grey: the account name beside the avatar could not be read on a light theme. The bar already re-mapped foregrounds to its on-accent colour, but for a class nothing produces any more - the account area is a menu region, while the rule still named the element it replaced. It now matches the text elements actually rendered, so anything textual raised onto the bar is legible. The account area also hovered in the generic light hover colour, which left that on-accent text unreadable at the moment of pointing at it; on the accent bar it now hovers and opens in the accent's own darker shades.
Defect: an overlay was indistinguishable from the page on a dark theme
A popup floating above the page painted itself in the page's own surface colour, edged it with the subtle border and shadowed it with a literal rgba the theme never saw. On a light theme that passes; on a dark one the popup and the page behind it differ by four steps of grey and the shadow is invisible, so a menu had no discernible edge.
Popups now take the elevated surface, the strong border and the theme's own menu shadow - a token that was configured and unused. The elevated surface equals the page's on a light theme, so nothing changes there. Swept across the menu, the toolbar drop-down, the sidebar's group flyout and a field's option list, the last of which keeps the field colour it shares with the input it belongs to; the convention is written down beside the spacing model rather than in each rule.
Defect: every image was announced as a photograph
The image element announced whatever it displayed as a photograph - the case the underlying photo viewer was written for. The QR code enrolling an authenticator is not a photograph, and a reader who cannot see it learns nothing from being told it is one. An image element now takes an alt text, which the photo viewer prefers over its own default; left unset, the default stands.
Defect: a base view naming an overlay operation was unloadable
ViewLoader classified a view as an overlay fragment by searching the raw file text for config:operation, so a base view naming that attribute in a comment was taken for an overlay of itself, had no base to extend, and every dialog opening it failed - silently, because no ErrorSink is available on that path, leaving a dead menu entry. That is what settings.view.xml did by documenting, in its own header, the overlay it expects contributors to write. The decision now comes from the parsed document, where only an actual attribute in the configuration namespace counts, named through the existing ConfigurationSchemaConstants.
Defect: a language change did not reach the running session
Changing the language stored the choice and had no visible effect until the next login - which is why the classic UI offers no way to switch at all, only a form field that takes effect the next time you log in. Two layers were responsible.
Resources caches the bundles of one locale on the sub-session and reused them while isValid() held - and that tracks a reload of the resources module, not the locale the session asks in. So a session kept answering out of the bundles it was entered with, and setCurrentLocale changed nothing. The cached value is the bundles of one locale, so the cache check now validates the locale as well. Only Resources itself writes that cache, in getInstance and in withLocale, and both store the bundles of the sub-session's current locale, so the check follows what the cache already meant.
That alone left everything created after a switch following it - a dialog, a snackbar, the client's own strings - while the labels of the tree already on screen stayed as they were: a control resolves its labels when it is created, and a reload renders the tree the tab still holds rather than building a new one. A rendered tree is therefore a rendering of a view in a language, which is what RenderedView now records: the reuse check compares the locale beside the view path and the loaded element, so a language change rebuilds where an ordinary reload keeps what the user produced. Losing a scroll position to a language change is the point at which those two interests part.
ViewServlet also wrote lang="en" on the document however the page was rendered; it now names the language the page is actually rendered in.
Defect: a tab was labelled in whichever language loaded the view first
TabBarElement and BottomBarElement resolved their labels in the constructor. An element is parsed once and cached for the life of the JVM, shared by every session, so the text was the one language whichever session happened to load the view first asked in - the demo's tabs read German to an English user, and no language switch could move them. The entries keep the ResKey and resolve it where SidebarElement already did, when a control is built for the session being served.
Defect: a switch of sidebar entries composed a URL of no page
The sidebar reported the new item before the page being left was detached, so between the two the routing participant of the old page - the tab bar naming the tab it showed - was still registered and read as belonging to the new one. The wrong composition reached the address bar, not only the log. The page being left now goes first, and nothing observes the two together.
Verification
Verified in the browser on the React demo application, which the change also converts from a flat user label plus Login/Logout toolbar buttons to the user menu.
- Anonymous: Login offered, no Change password / Settings / Logout, and no language entries. Logged in: the account name and avatar closing the app bar, the entries grouped by separators into leaving, the account's own settings, the languages and the themes - and the anonymous menu opens with no stray separator.
- Theme: switching applies instantly with no reload, in both directions, the active theme offered as disabled each time; the entry icons render.
- Language: switching applies within the session, in both directions, without a re-login - sidebar navigation, panel title, table toolbar, column headers, menu entry labels and date formatting all follow, and the choice survives a reload and a re-login. A plain reload still keeps a table's selection; only an actual language change resets it.
- Flags: each language entry carries its own flag, and each of the 249 options of the country field carries the flag of that country, as does the selected one; every code resolves against the stylesheet. An enumeration's options carry no icon and are not indented by one, and an account option carries the icon its type is configured with.
- Settings: one button bar per dialog, carrying Save beside Close on the account and profile tabs and Close alone on the security tab, on a freshly opened dialog as well as after switching tabs; the security buttons are inset like a form. Timezone and country persist to the account.
- Start page: logging in from an anonymous session sitting on another page lands on the start page of the account logging in.
- App bar: the account area closes the bar at both a wide and a narrow viewport, with the drawer toggle leading it, and the command buttons stay flush against it rather than mid-bar.
- Right-click context menus still work, checked on the View Designer tree, including per-node executability.
- No JavaScript errors and no server-side errors throughout.
Migration
Renamed. An application referring to these must follow:
| com.top_logic.layout.view.command.ContextMenuRegionControl | com.top_logic.layout.view.command.MenuRegionControl |
| React module TLContextMenuRegion | TLMenuRegion |
| ContextMenuElement.DEFAULT_TARGET_CHANNEL | AbstractMenuElement.DEFAULT_TARGET_CHANNEL |
MenuRegionControl also takes a list of ContextMenuContributions instead of a single one, and a MenuTrigger. The <context-menu> element and its configuration are unchanged, so no view file needs editing.
Removed. com.top_logic.contact.layout.CountryResourceProvider is gone, together with the two contactConf.config.xml registrations of it. An application referring to the class must drop the reference: a country is served by com.top_logic.util.CountryResourceProvider, registered in top-logic.config.xml, which produces the same label and adds the flag. An application that registered the removed class for a type of its own must name the new one instead.
Signatures changed. ReactMenuControl.MenuEntry gained a cssClasses component, so a call of its canonical constructor needs the extra argument; the item(...) and separator() factories are unchanged and are the intended way to build one. UITheme gained an icon constructor parameter. ReactAppBarControl gained a trailing control parameter; the shorter constructors are unchanged and place nothing at that end.
Behaviour changed, no code change needed.
- display and css-classes on a view command now take effect wherever the command is rendered. An application that relied on them being ignored will see its app-bar buttons change presentation, and one that worked around the gap in CSS (selecting a command's button by its icon) should drop the workaround in favour of css-classes.
- An option of a drop-down now shows the image its type registers a ResourceProvider for, in the option list, the chips and the read-only display. An application whose types carry an icon will see it appear in every select; one that does not want an icon on a particular type registers ThemeImage.none() for it, which is rendered as no icon rather than as an empty one.
- The language of a session is no longer fixed at login. An application caching locale-dependent state per session must invalidate it when SubSessionContext#getCurrentLocale() changes, and one resolving a ResKey while constructing a shared object (a UIElement, a cached model) must move that resolution to where the object is rendered for a session.
- A language change rebuilds the control tree of the browser tab, discarding transient view state (scroll position, expansion, unsubmitted form input) that is neither in a channel nor in the personalization. An ordinary reload keeps it, as before.
- Logging in or out no longer keeps the page the session was on. The account taking the session over starts on its own start page, or on the application's default where it has chosen none.
- A popup - a menu, a toolbar drop-down, a sidebar group flyout, a field's option list - is painted from the elevated surface, the strong border and the theme's menu shadow. A theme that defines layer-02, border-strong or shadow-menu differently from the standard themes will see its popups follow those values.