enhancement
major
minor
major
minor
major
#29544
TL Views table: row activation by double-click or Enter, and a grouping UI over the existing table model
Problem
Two gaps of the <table> element (TableElement, com.top_logic.layout.view) and its control (TableViewControl, com.top_logic.layout.react), both observed in an issue tracker built on the view layer.
1. A row could not be activated
The commands the table control accepted were select, selectByKey, moveSelection, selectAll, sort, scroll, expand, the column and filter commands. There was no command for "open this row": a list opened its detail only by selecting the row and then pressing a toolbar command, two clicks and a mouse trip for the most frequent interaction in a list-driven application. Double-click and Enter did nothing. The automatic links of #29532 cover reference values displayed in cells, not the row itself.
2. Grouping existed in the model but reached neither the control nor the configuration
The table model groups already: TableView.group(GroupSpec), RowSource.withGrouping, single-column grouping in ListRowSource, aggregated group rows in DefaultTableView, and the grouping is part of the persisted TableViewState. But TableViewControl had no state field and no command for it, the client rendered no group rows, and TableElement.Config offered no way to declare a grouping.
3. Activation was reachable by mouse and keyboard only, and the tree had no keyboard selection
Double-click and Enter are the only activation gestures; neither exists on a touch device, and neither is visible, so a user has to know about them. Lists that open a detail frame, such as the table tiles of the drill-down demo, therefore still carried an explicit "Open" toolbar button. In the tree, the arrow keys moved only a client-side focus mark: selection did not follow, so a single-selection tree could not be operated by keyboard without activating, and Enter acted on the focused node while the selection channel held another.
Solution
Row activation
<table> and <tree> get an activation command, an ordinary ViewCommand configuration run with the activated row (or the node's business object) as input:
{{{#!xml <table rows="…" selection="ticket">
<on-activate>
<open-dialog dialog-view="ticket-detail.view.xml" bind-input-to="model"/>
</on-activate>
</table> }}}
- The client sends activate with the row on double-click and on Enter while the row carries the keyboard cursor (a tree node: double-click or Enter on the focused node, which a click focuses too); the server selects the row first, so the selection channel holds it, and then runs the command with the row as input. The command's own executability rules apply; a row for which it is not executable activates nothing. A double-click inside an interactive cell element belongs to that element.
- The activation is recorded for scripted tests as activateByKey naming the row's business object, like select.
- ViewCommandModel.forCommand(ViewContext, ViewCommand, ViewCommand.Config) and ViewCommandModel.execute(ReactContext, Object input) run a configured command with a caller-supplied input; the button, the toolbars, the app bar and the activation share this one construction of a command model.
Grouping UI
- TableElement.Config gets group-by="column" for the initial grouping; the user changes it from the column header menu ("Group by this column", "Remove grouping") and from the column selector, where each column carries a group toggle. The choice is persisted with the rest of the table state under the table's personalization key.
- TableViewControl exposes the grouping in its state and accepts a group command. Group headers are delivered as expandable rows at depth 0 with their members at depth 1, through the tree rendering the client already has, so collapsing a group is collapsing a node. A header shows the group value rendered by the grouped column's own cell renderer (a classifier as its label, a date in its format), the member count, and in every other column the aggregate the column's Aggregator yields, or nothing. A header stands for no object: select and activate on it toggle its expansion, and the selection channel never receives a group. Grouping and ungrouping keep the selection, since the data rows are unchanged; only a key no longer among the rows is dropped (#29535).
- Sorting applies within groups, filters before grouping. Multi-column grouping stays out of scope; the UI offers one column.
Activation button and keyboard selection
- A <table> with an <on-activate> command shows a pinned trailing column with one chevron button per row that runs the activation command for that row. The button follows the command's executability rules per row (a rejected row shows a disabled button, a hidden one no button), its tooltip is the command's label ("Open" when the command declares none), and it makes activation visible and usable on touch devices, where neither double-click nor Enter exists. The column is not selectable, groupable, resizable or freeze-eligible; a view that wants the gestures only switches it off with activation-button="false".
- The button column is an instance of a general row-command column (RowCommandColumn, com.top_logic.layout.view.table): one pinned column per ViewCommandModel a caller offers on every row, rendering an icon-only button through the cell-control seam (CellContent.Raw with a CellControlFactory). ViewCommandModel.executability(Object input) exposes the per-input executability the button decides its state by; TableElement builds one model for <on-activate> and hands it to both the activation gesture and the column, and RowSetTableControl.setTrailingColumns(…) appends such columns in the editable table variant.
- A Column can be pinned to the end of the table (Column.pinnedEnd(), DefaultColumn.Builder.pinnedEnd(true)). A pinned column is neither frozen-eligible nor selectable; DefaultTableView keeps it behind every other column whatever the persisted order says, the frozen prefix never reaches it, and moving, resizing, hiding and grouping by it are declined. The client renders such columns sticky at the right edge of header and body, so they stay visible while the table scrolls horizontally; the column that stretches to fill the remaining width is the last unpinned one. Freezing columns at the left stays as it was.
- The tree adopts the table's keyboard model: the arrow keys, Home and End select the node they move to in single-selection mode; in multi-selection mode they move the cursor only, Shift extends the selection and Space toggles the cursor node. Enter activates the cursor node, so the activated node is always the selected one.
Display of the command column, cursor ring, fit width to content
- The rows and the header row fill the table even when the columns are narrower than it: the last column the user arranges grows into the rest, in the heading exactly as in the rows, so a column pinned to the end (the chevron column) always sits flush at the right edge. The strip the column selector's cog reserved at the right of the header is gone wherever the table ends in a pinned column: the cog then sits in that column's heading, which carries no label anyway. A table without a pinned column keeps the cog over the right edge of the header as before.
- A Column can carry a column-wide CSS class (Column.cssClass(), DefaultColumn.Builder.cssClass(String)), which the client puts on every cell of the column, its heading included. RowCommandColumn uses it to drop the text padding of its cells and to center a frameless command button with room around the icon; the frame appears under the pointer only.
- The ring marking the keyboard cursor row is drawn on the cells instead of the row, so the frozen columns at the left and the pinned ones at the right, which are sticky and painted over the row, show it too.
- Fit width to content: "Fit width to content" in a column header's menu and a double-click on the column's resize handle set the column to the width of the widest cell content currently displayed, the heading included, and persist it through the table personalization like a width dragged by hand. Pinned columns keep their fixed width.
Also fixed on the way
DefaultTableView told its row source about the initial sort but not about the initial grouping, so a configured grouping was stored in the state and never applied; both are pushed to the source now.
Documentation and demo
docs/faq/react-view-layer.md documents row activation, the activation button, pinned columns, the column-level CSS class, the fit to content and grouping. com.top_logic.demo.react: the object list groups its tickets by status; the Attributes table opens the detail dialog on activation and offers grouping from its header menu; a Tree Demo view activates nodes; the account tables of the tile drill-down demos push the person frame on activation (<on-activate class="…NavigatePushCommand">) instead of through an "Open" toolbar button. Tests: TestTableActivation, TestTableGrouping, TestPinnedColumns (tl-layout-react), TestTableElement, TestTreeElement, TestRowCommandColumn (tl-layout-view), TestGrouping (tl-core).