XObjects display macro

Last modified by Manuel Leduc on 2026/10/06 12:27

 XWiki
 Design
 Active
 
 

XClassPickerImprovements
https://hr.xwikisas.com/xwiki/bin/view/Seminars/Seminar2025/Seminar2025/Hackathon2025/no-script-class-sheets#Attachments (XWiki SAS for a property displayer macro, also involving a XClass picker, not publicly accessible)
https://github.com/xwiki/xwiki-platform/pull/6374 (draft pull request of XWIKI-24831)

 
No

Description

Abstract

A macro that lists the entries of one object type — the XObjects of a chosen XClass — as a Live Data table whose columns are a free mix of the entry's own fields and its page metadata. It is configurable entirely from the WYSIWYG macro dialog, with a picker for the object type and widgets derived from it for the columns, the filters and the sort, so an author who does not write XWiki syntax can build a sortable, filterable report over structured data anywhere in the wiki.

A Live Data table of client projects with columns Page, Client, Phase, Owner, Budget and Modified, a filter row, and pagination showing entries 1 to 5 of 24.

This page specifies option 2.1, which is the option the forum discussion converged on, and the one implemented by XWIKI-24831 (draft pull request #6374). The first version implements a subset of this page: what it does not implement is marked Not in the first version — planned future work. The options that were considered and set aside are kept, condensed, in Rationale and rejected alternatives.

Motivation

Listing structured data is not the missing piece — authoring a listing where the need arises is.

App Within Minutes already generates a listing of an application's entries on the application home page, and that listing can be edited. What it cannot do is follow the author: a project manager who wants "the meetings I was involved in last December, with only the title and the executive summary, sorted by date" wants that table in their own document, next to the text that explains it, not on the application home page. Today that means either editing the generated home page (advanced users only, and only one central listing), or inserting the Live Data macro by hand.

The Live Data macro can express all of it, but it is deliberately generic: it exposes a source, source parameters, a properties list and a filter query string, and it validates none of it against the object type the author has in mind. The documents (pretty name: Pages) macro is friendlier but is about pages, not about structured data, and offers no completion either. For an author who is not an advanced user, neither is usable without being told what to type.

This macro is one of three bricks that move the App Within Minutes capability in-place, to wherever the author needs it:

  • (done, 18.7.0) Live Data edit mode, to add, modify and remove entries from the table itself.
  • (this proposal, implemented by XWIKI-24831) A macro to place a custom listing of an object type in any document.
  • (future) Creating an object type directly from a Live Data.

Once the three exist, an App Within Minutes application is largely an object type plus a page holding this macro, and the generated home page should itself use it. Deciding what then happens to App Within Minutes — and how these features stay discoverable — is out of scope here.

This also matters for users arriving from Confluence and Notion, where such listings are routine.

Vocabulary

This page uses the end-user vocabulary discussed in End users XClass and XObject vocabulary:

ConceptIn the API and in this page's technical notesIn the UI
The definition of a structureXClass, classobject type
One instance of itXObject, objectentry
One of its memberspropertyfield

This macro uses object type, as decided in the forum discussion of this proposal and aligned with the label of the Solr search facet (solr.field.class=Object type); one instance of it is an entry and one of its members is a field. The wider vocabulary discussion is not concluded, and the migration is expected to be a full pass over the UI and the documentation in a major release: if it settles on another term, this design changes wording and nothing else. The parameter names in the macro descriptor deliberately stay technical and stable (class, properties), because they are syntax, not UI.

Use cases

  • UC1: A non-advanced user (e.g., a project manager) inserts a listing of the entries of an object type someone created, without knowing the class reference or that a class exists.
  • UC2: An author who does not know XWiki syntax configures the table entirely from the WYSIWYG macro dialog, with suggestions at every step.
  • UC3: A reader filters and sorts the table themselves, without editing anything.
  • UC4: An author restricts the table to one part of the page tree, so that only the entries stored under a chosen page are listed, at any depth below it. (The narrower "direct children only" variant stays out of scope, see G6.) Not in the first version — planned future work.
  • UC5: An author wants a plain list of pages with no object type at all. Equivalent to the {{documents}} macro. Already covered by the documents macro
  • UC6: An author reopens the macro months later to add a column. The dialog opens on the configuration they left — the picked object type, the chosen columns in their order, the filters — not on defaults, and adding one column changes nothing else.
  • UC7: (optional, probably best covered by a contrib extension) A Confluence migration turns a Page Properties Report into this macro, with the migrator creating the class.
  • UC8: A field is renamed or removed from the object type after the macro was configured, and the page still renders, minus that column, with a legible explanation.
  • UC9: A reader without view rights on some of the matching entries sees a table containing only what they may see, with a consistent count.
  • UC10: A reader with edit rights adds entries to the table, and edits existing ones, in place, if the author allowed it. Not in the first version — planned future work.
  • UC11: An author picks an object type, configures five columns, then changes their mind about the object type. The columns cannot survive, and the author is told before losing them rather than after.

Specification

Requirements

  • R1: Rows are the entries of a chosen object type, one row per page holding at least one object of it, showing the first object of the class on that page (see the known limitation); columns are a free mix of the entry's page metadata (doc.*) and that object type's fields. (UC1)
  • R2: The macro is fully configurable from the WYSIWYG dialog; no parameter requires handwritten syntax. (UC2)
  • R3: Each column is sortable and filterable where the underlying field allows it, with the filter widget derived from the field type. (UC3)
  • R4: Live Data is the rendering and interaction layer, i.e., no table re-implementation. (UC3, UC10)
  • R5: Column order is authored and preserved as authored. (UC1)
  • R6: (optional) Omitting class reproduces the current {{documents}} macro behavior. (UC5)
  • R7: The configuration round-trips: the dialog reopens on the saved state, and reopening then saving without edits changes nothing. (UC6)
  • R8: The row set can be restricted to the entries stored under a chosen location, at any depth below it. (UC4) The semantics are exact: a location scopes to that page and its descendants, and to nothing else. That is deliberately not what the liveTable source's own location parameter does (G6), so the macro ships its own Live Data results page to express it — see Mapping to Live Data. Restricting to direct children only stays out of scope. With no location set, the macro lists the entries of the chosen object type across the whole wiki, subject to view rights. Not in the first version — planned future work.
  • R9: A missing object type or field degrades to a legible message, never a stack trace, and never silently drops the whole table. (UC8)
  • R10: View rights are enforced on rows and reflected in the result count. (UC9)
  • R11: The rendered table is paginated and must not scan the full result set to display its first page.
  • R12: The table carries a description, shown above it and referenced as its accessible description, and keyboard navigation is not worse than a bare Live Data.
  • R13: It is possible to see the impact of a change of macro property while editing the macro. Deferred: this is what separates option 2.2 from 2.1. It is worth having, but as a generic macro preview, not as an implementation private to this macro. Not a requirement of this proposal.
  • R14: It is possible for readers to add entries and to edit existing entries if their rights allow it. (UC10) Not in the first version — planned future work.
  • R15: The UI leads with entries of an object type and mentions pages only where they matter: the title as the entry name, the location, the rights and the page metadata columns, which are offered as such. The page a given entry lives in stays reachable, and the vocabulary is the one in Vocabulary. (feedback from the forum discussion)
  • R16: The dialog itself is accessible: every parameter widget has a programmatic label and its description, the picker suggestions are operable from the keyboard, and a disabled parameter states why it is disabled rather than only looking dimmed. (UC2)
  • R17: A parameter whose valid values depend on another is not offered until that other parameter is set, and insertion is blocked while a mandatory parameter is empty. (UC2) Concretely: every tab of this macro is derived from class, so the tab strip is hidden until an object type is picked and appears once one is. The macro editor cannot disable a group, but it can hide one, and a tab that is reachable with nothing to offer is worse than no tab: it invites the author to look for something that is not there yet.
  • R18: Every dependent parameter has a defined standalone fallback, so the macro is shippable before the dependent-parameter mechanism exists, with degraded authoring rather than a missing feature. (UC2) Superseded: the dependent parameters are client-side suggest pickers that read the object type at call time (see G1), so neither the dependent-parameter mechanism nor a plain-input fallback is needed.
  • R19: Changing the object type invalidates the parameters derived from it, and the author is warned before the change is applied, with a native confirmation whose text is translatable. (UC11)
  • R20: The design uses only what the macro configuration framework and the Live Data modules provide today, or names the gap explicitly. Every gap found is listed in Technical gaps with what it blocks.

Macro identity

  • Macro ID: records
  • Macro name: Records
  • Macro description: Displays a collection of entries of the same object type, as a table readers can sort and filter.
  • Category: Content
  • Supports inline mode: no — the macro renders a table.
  • Module: xwiki-platform-records, in xwiki-platform-core, following the established xwiki-platform-<feature>/xwiki-platform-<feature>-macro pattern, with two submodules: xwiki-platform-records-macro (the macro) and xwiki-platform-records-webjar (the parameter pickers its dialog uses). R8 will add a third: the macro will ship a Live Data results page, which is a wiki page, so it will need a xwiki-platform-records-ui XAR alongside the jar. Not in the first version — planned future work.
  • Content: none in this proposal. Live Data's own macro accepts an advanced JSON configuration in its content; exposing the same here would create a second, higher-fidelity way to express the same parameters, and the two formats do not merge cleanly (see Mapping to Live Data).

Naming

The forum discussion found records too generic, and the point stands — but it applies to two things with opposite costs, and separating them resolves it.

What it isCost of changing it later
Macro idSyntax. It is written into every page that calls the macro, and into the module's Maven coordinates and extension idIrreversible. It must be decided before the first release
Macro name and descriptionTranslation keys. They are what the quick action and the macro search match onFree. They can change in any release, and should be iterated on

Both objections raised on the thread — "Record is not bad I just find it too generic" and "I would expect users to look for a macro that has data in its name" — are about discoverability, which is a property of the name and the description. The macro id is never seen by the authors this macro exists for. The platform's closest analogue makes the point: the documents macro's id is documents, and the name an author actually reads is Pages (rendering.macro.documents.name=Pages); likewise liveData is displayed as Live Data. Platform macro ids are single lowercase words or camelCase, never hyphenated, so records also fits the house style.

So this proposal closes the id as records, the name as Records and the description as Displays a collection of entries of the same object type, as a table readers can sort and filter., as decided in the forum discussion. The name and the description remain translation keys, so they can still be adjusted in any release, but they are no longer an open question. Both the quick action and the macro search index the name and the description, so the description is where the words an author is likely to type (data, collection, entries, table) are caught.

One tension is worth recording rather than hiding: record is the word the App Within Minutes revamp proposes for an XObject, whereas the vocabulary discussion converged clearly on entry (Q3 drew +1 from five people, with no opposition). The macro id therefore uses a third word for the thing the project has just agreed to call an entry. What makes that acceptable is the same asymmetry as above: the id is invisible to authors, and the vocabulary that reaches the UI — every label, hint and message in this design — stays entry / field regardless. Building the id on the unresolved half of that discussion (the word for XClass, which drew no consensus and whose migration is targeted at 19.0.0) would instead make it a hostage.

Macro descriptor

ParameterTypeMandatoryAdvancedDefaultGroupDescription
classString (XClass reference)YesNo(none)(none)The object type whose entries are listed. (R1)
locationString (page reference)NoNo(none)DataLists only the entries stored under this page, at any depth below it. Empty means the whole wiki. (R8, UC4) Not in the first version — planned future work.
propertiesString (comma-separated)NoNodoc.title, then all fields in class orderColumnsThe columns to fetch and display, in the order given. Fields and doc.* metadata. (R1, R5)
filtersString (query string)NoNo(none)Filter & sortFilters applied before the reader sees the table. (R3)
sortString (comma-separated)NoNo(none): the order the object type returnsFilter & sortThe columns the table is sorted on initially. Each name may be suffixed :asc or :desc.
layoutsStringNoNotableDisplayThe layout of the table: table or cards. Building the list of choices dynamically from the registered Live Data layouts is planned future work.
limitIntegerNoNo15DisplayEntries shown per page. (R11) Not in the first version — planned future work. Live Data's own default applies, and the reader changes the page size from the pagination controls.
descriptionStringNoNo(none)DisplayShown above the table, and used as its accessible description. (R12)
editableBooleanNoNofalseDisplayLet readers add and edit entries directly in the table, where their rights allow it. (R14) Not in the first version — planned future work.
idStringNoYesgeneratedAdvancedIdentifier for this table, needed only when a page holds more than one. A missing or duplicate identifier is made unique silently, not rejected. It is not only about keeping reader state apart — see G12.

Three changes with respect to the earlier revisions of this page are worth calling out:

  • sort and filters are no longer advanced. Choosing the filtering and the sorting is one of the three things the proposal exists for; hiding them behind the advanced flag contradicted the motivation.
  • location is back in the specification, with R8 (but not in the first version), with exact semantics rather than the liveTable source's substring match — which is why it costs a results page (G6). nested stays gone: once the semantics are stated precisely there is no choice left to expose.
  • translationPrefix is gone. No macro in the platform exposes it, and it silently overrides the field names the object type's author chose — see Internationalisation. Records sets a prefix internally for its doc.* headers, exactly as the documents macro does, and does not surface one.

class is the only mandatory parameter. In the first version it has no group and is rendered as a bare mandatory field above the tab strip, because a panel headed Data wrapping a single field would be redundant. Once location exists, the two will share the Data group, and a group is mandatory as soon as one of its parameters is, so Data will be rendered flat as a panel above the tab strip rather than as a tab: the two parameters that decide which rows the table has sit together, at the top, where the author starts. The four remaining groups — Columns, Filter & sort, Display and Advanced — become the tabs, in that order. None of that is a free choice — see Authoring flow.

Authoring flow

The dialog's shape is not free: it is what MacroDescriptorUIFactory and macroEditor.js produce from the descriptor. Two rules decide the layout, and both were checked against the code:

  • Parameters carrying a @PropertyGroup are collected into that group whatever their mandatory flag, and a group is marked mandatory as soon as one of its parameters is. Mandatory nodes are rendered flat, above the tab strip, as a Bootstrap panel titled with the group name plus (required); optional nodes become the tabs. So a Data group holding the mandatory class (alongside location, once it exists) cannot be a tab: it is the panel at the top, and class alone is that flat field. The tabs are Columns, Filter & sort, Display and Advanced.
  • Group order is not freely choosable from Java. Tabs sort by group order ascending, -1 last, ties by group id, and a group's order is the lowest order among its parameters. But MacroDescriptorUIFactory pins the defaultOptionalGroup ("Optional parameters") at order 0, and DefaultBeanDescriptor#handlePropertyOrder stores the annotation value only if (value > 0) — so @PropertyOrder(0) and negatives are silently dropped, and no Java macro can place a tab before "Optional parameters". The lever that does work is to give every optional parameter an explicit @PropertyGroup, leaving the default group childless so it is never added to the tab strip at all. That is what this design requires in order to get the tab order above, and it is why id carries an explicit Advanced group rather than being left in the default one: a single ungrouped optional parameter would pull "Optional parameters" back into the strip as the first tab, ahead of Columns. (Wiki macros are not affected: DefaultWikiMacroFactory reads the order with getIntValue(…, -1) and propagates it when > -1. The asymmetry is worth its own issue.)
  • There is no disabled state for a parameter or a group, but there is a hidden one. hidden exists (@PropertyDisplayHidden with no value set, or a deprecated parameter with no value set), and disabled exists only inside a @PropertyFeature radio choice. A tab that is "not available yet" therefore cannot be drawn as padlocked or greyed out; it is hidden until it has something to offer, which for this macro means until class is set.

That second rule is what shapes the dialog's two states below: with no object type picked there is only the Object type field, and the tab strip appears with the object type. It is a better outcome than the greyed-out tab it replaces, which the editor cannot draw anyway.

A third constraint decides how much room there is for all of it: the macro editor modal is a plain .modal-dialog — modal.js adds no modal-lg, and macroWizard.css sets no width — so it is Bootstrap's default 600 px, and the macro-editor-modal class is shared by every macro, so widening it for this one is not possible today. The mockups below are drawn at that width, which is why the column chooser and the filter rows are laid out as tightly as they are. If the dialog turns out to be too cramped in practice, a per-macro dialog width is a platform change to ask for, not something this macro can decide.

Step 1 — the object type

The Records macro dialog as it opens. The Object type field, marked required, is focused and empty above the tab strip. Below it, the Columns, Filter & sort, Display and Optional parameters tabs, with the Columns tab showing an empty state reading “No object type chosen yet.”

The dialog as it opens: class focused and empty, and no tab strip at all, because every tab is derived from the object type. (R17) Picking one reveals the tabs. Every state visible here is specified in Interaction rules.

The mandatory class decides which rows the table has. The optional location, which scopes the table to one part of the page tree (R8), is not in the first version — planned future work; the two will then sit together in a Data panel above the tab strip. location will be a page picker rather than a text field — the value is a reference, and picking it removes the whole class of typos that a hand-typed reference invites. Leaving it empty lists the object type's entries across the whole wiki, which is the default and the common case.

Warning

The mockups on this page predate the implementation, and where they contradict it they are superseded by the screenshots of the pull request. In particular they show the Data type wording, a rich class picker, a two-panel transfer list, filter rows with an operator, layout checkboxes, an entries-per-page select, an editable checkbox and a plain-input fallback, none of which is in the first version; and the last tab still labelled Optional parameters and still carrying the translationPrefix field that this revision removes. They are kept as the record of what was discussed.

The same dialog with the object type picker open on the query “proj”, listing Client project (142 entries), Project review (38 entries) and Project risk (0 entries) with their locations, a field summary for the highlighted entry, a “3 of 3 matches” footer and a checkbox to also show technical object types.

The picker searching on proj. What the macro needs from it: matching on a human-readable name rather than a reference, the location to tell two similarly-named types apart, the number of existing entries to tell a real object type from an internal one, and technical types hidden unless deliberately asked for. The picker is not designed here, and the first version does not improve it: it uses the existing class picker, and improvements to that picker are out of scope for this page. They are the subject of the separate proposal XClasses picker improvements (R1 to R7 there cover exactly this list), discussed in improve the information presented in the XClass picker and improve the listing of XClass in the XClass picker.

The Object type field with Client project selected, shown as a chip carrying its location and entry count with Change and clear buttons, and the Columns tab below now offering the transfer list.

Once an object type is chosen it is displayed as what was picked, not as a reference: title, location, entry count — the location is shown to tell two similarly-named types apart, not to scope the table. Choosing it is the single interaction that brings the four tabs to life. (That chip is the improved picker of the separate proposal; the first version shows what the existing class picker shows, see the pull request for its screenshots.)

Step 2 — the columns

The Columns tab showing a two-panel transfer list. The Available panel groups Entry metadata (Location, Created by, Creation date, Last author) and Client project fields (Budget, Start date, Status), each row carrying its field type. The Displayed panel lists Title, Client, Phase, Owner and Last modified with drag handles.

An ordered suggest picker (a multi-value select whose chosen items can be dragged to reorder them and removed one by one). The two groups matter: page metadata and the object type's own fields are different things and authors look for them separately, so the candidates are offered in two groups, Page metadata and <object type> fields. Each candidate shows its field type, because "Owner" as a user field and "Owner" as free text do not filter the same way. Free text is refused, since a column the object type does not have would render empty. The chosen items are the value of properties, in their order. When none is chosen, the placeholder Title and every field states the default. (R1, R5) (The mockup above shows an earlier two-panel transfer list and is superseded by the pull request screenshots.)

The candidates come from the Live Data properties REST resource, with no new backend:

GET /xwiki/rest/liveData/sources/liveTable/properties?sourceParams.className=Clients.Code.ProjectClass

The sourceParams. prefix is how AbstractLiveDataResource passes source parameters through, so the class is expressible in the request. LiveTableLiveDataPropertyStore builds the descriptors from the XClass, and each one already carries the pretty name, the type, and the sortable / filterable flags. A field the source reports as neither sortable nor filterable is still offerable as a column; it simply gains no filter row.

Step 3 — filters and sort

The Filter & sort tab. Three filter rows pair a field, a non-editable operator and a value: Status contains Active, Status contains On hold, and Client contains an empty value. A hint explains the operator is the field type's and cannot be changed here. Below, a Sort section with Start date and an Ascending/Descending toggle set to Descending.

Note: this mockup is superseded by the pull request screenshots where it contradicts the implementation: the filters are field=value suggester items with no operator, and the sort is a suggester with an ascending and a descending candidate per field.

A multi-value suggest picker with one item per filter, written field=value, suggested in two steps. Step one suggests the fields of the object type — all of them, not only the displayed columns, and never the _-prefixed pseudo-columns. Typing = starts step two, which suggests that field's own values, read from the filter.searchURL of its property descriptor: the same URL that feeds the Live Data filter row, so the dialog offers what a reader is offered. Free text stays allowed, for the values that cannot be enumerated. Which filter a field gets, and its operators, is what the liveTable source already declares for that field type — liveTableLiveDataConfiguration.json maps every XClass property type onto a filter:

FilterXClass property typesOperators available
textString, TextArea, Email, Timezonecontains (default), startsWith, equals
listStaticList, DBList, DBTreeList, Page, Groups, Levels, Usersequals, startsWith, contains (default), empty
dateDatebetween (default), before, after, contains
numberNumberequals (narrowed to equals only by the liveTable source)
booleanBooleanequals
(none)Password, ComputedFieldnot filterable, and not sortable

Reusing those descriptors is deliberate: every filter type XWiki ships works here for free, and there is no second implementation of which values a field can take to keep in sync with what the reader will see. (R3) The dialog does not validate a typed value against its field type.

The operator is not editable in this dialog, which does not show it either. filters is a query string, and LiveDataRendererConfiguration#getFilter builds each constraint as new LiveDataQuery.Constraint(value) — no operator, so the field type's default is what applies. Several values on the same field are representable (status=Active&status=On%20hold becomes two constraints on one filter), which is why the same field may appear in several items; the values are percent-encoded when written into filters; what cannot be represented is this constraint uses startsWith. Either the parameter grows an encoding for it, or the dialog keeps offering only what it can store — this design takes the second option, and the open questions record the first.

The sort is a suggest picker offering two candidates per field, <Field> (ascending) and <Field> (descending), among all the fields of the object type, not only the displayed columns. A field already used disappears from the list in both directions, since a second criterion on the same field does nothing. That one-criterion-per-field rule is enforced in the dialog only: a hand-written sort value is accepted. The words ascending, descending and default order are still hard-coded English in the webjar, which has no access to a translation bundle yet.

Step 4 — display

The Display tab with Table and Cards layout checkboxes, an entries-per-page select set to 15, a description field reading “Acme projects, most recent first” with a note that it is announced to assistive technology, and an unchecked option letting readers add and edit entries directly in the table.

Note: this mockup is superseded by the pull request screenshots where it contradicts the implementation: the first version has two layout radio buttons, and neither the entries-per-page select nor the editable option.

Purely presentational, independent of the object type. The description is not decoration, but it is not hidden either: Live Data renders it as a plain paragraph above the table, in normal flow, and points the layout at it with aria-describedby. So it is the table's accessible description — not its name, which is a different thing and which the table does not currently have at all. An author needs to know it will be on the page, which is what the parameter now says. (R12) The editing switch, when it exists, will state plainly that rights are still enforced (R14). Not in the first version — planned future work.

The mockup below still carries the earlier, misleading note that the description is announced to assistive technology.

In the first version the tab offers two radio buttons, table (the default) and cards, with the layout names taken from Live Data's own translations; the list is fixed in the template. Building that list dynamically from the registered layouts is planned future work, as is the page-size select (limit). Both lists that dynamic version needs — the available layouts and the offered page sizes — are not exposed by any REST resource; the Live Data REST module publishes sources, properties, property types and entries, and nothing else. They live in liveDataConfiguration.json (meta.layouts = table, cards; meta.pagination.pageSizes = 15, 25, 50, 100) and are reachable server-side through $services.liveData.effectiveConfig(...), which is exactly where an HTMLDisplayer runs. So these two widgets are feasible, but only as server-rendered ones — a purely client-side widget would have to hardcode the values, which it must not.

The Advanced tab

The Optional parameters tab with a translation prefix field and a table identifier field, both marked “(advanced)” and empty with placeholder text, and a note that advanced parameters are marked rather than hidden.

Note: this mockup is superseded by the pull request screenshots where it contradicts the implementation: the tab is titled Advanced and only holds the table identifier.

One parameter, id, for the case the four other tabs do not cover: two Records tables on the same page. (The mockup still shows the removed translationPrefix field beside it.) The macro always sets an identifier, made unique silently through the document's identifier generator (a collision becomes <id>-1, and so on), rather than rejecting a duplicate.

Two framework notes. The tab is named Advanced and is a group of its own rather than the framework's default "Optional parameters" group, because leaving even one optional parameter ungrouped would put that default group first in the tab strip and there is no way to move it —see the third rule above. And the framework does not hide advanced parameters: displayMacroParameter marks them (advanced) next to their name, exactly as it marks (required) and (deprecated). The advanced flag is a signal to the author, not a way to get a parameter out of the way.

Parameter widgets

ParameterWidgetStatus
locationPage pickerExists: the standard page picker, used unchanged. Not in the first version — planned future work.
classObject type picker, as an HTMLDisplayer for a dedicated Java typeExists but developer-facing (the Velocity classPicker macro): unfiltered, uninformative. The first version uses it unchanged; a user-facing variant is out of scope for this page and is specified in XClasses picker improvements.
propertiesGrouped, ordered suggest pickerShipped in the first version: a custom suggester, no new backend, fed by DefaultLiveDataPropertiesResource. Grouping is proven — classPicker already groups with <optgroup>. See the three caveats in G1 (order, grouping, invalidation).
filtersTwo-step suggest picker, one field=value item per filterShipped in the first version: a custom suggester whose second step reads the values from the filter.searchURL of the property descriptor.
sortSuggest picker with an ascending and a descending candidate per fieldShipped in the first version. Same suggester route as properties.
layouts, limitlayouts: two radio buttons (table, cards). limit: select fed by $services.liveData.effectiveConfig(...)layouts ships with a fixed list; building it dynamically, and limit altogether, are planned future work and need a server-side widget: the layout list and the page sizes live in the Live Data configuration and no REST resource exposes them.
description, editable, idPlain inputsExist. (editable is not in the first version.)

Three of these widgets (properties, filters, sort) depend on the value of another parameter, which the macro editor does not support declaratively. All three are built as client-side suggest pickers that read input[name="class"] when asked for suggestions, with caveats. See G1.

Note for whoever implements them: the macro editor extracts parameter values with serializeArray over :input (macroEditor.js), so a custom widget must write its value into an input named after the parameter, whatever it renders on top.

Alternative: reuse the Live Data panels instead of building three widgets

Live Data already ships the three controls this design is about to re-implement — its own filter, sort and properties panels, which a reader uses to adjust a table in view mode. Building three lookalikes next to them deserves a justification, so here is the evaluation.

What makes it possible. Two things, both checked:

  • The macro editor modal lives in the parent document, not in the standalone editor's iframe. The script filtering that strips macro JavaScript inside the editing area therefore does not apply, and Live Data's own JS runs normally in a parameter widget.
  • logic.registerPanel is public API on the Live Data logic object, and LiveDataAdvancedPanelExtension already exists so that an extension can host a foreign DOM container as a panel.

Why it is more than a shortcut. A widget that owns class, properties, filters and sort together sidesteps G1 entirely: there is no sibling parameter left to read, because one widget holds all four values. And the macro editor permits exactly that — extractFormData scrapes :input across the whole modal into one flat name-to-value map, and toMacroCall then reads that map by parameter id. A single widget writing hidden inputs named properties, filters and sort has those values picked up, whichever widget rendered them. There is also a real usability argument: the author would configure the table with the very control the reader later uses to adjust it, instead of learning a second, author-only idiom.

Why it is not the specification here. Four obstacles, two of which are new gaps:

ObstacleDetail
The panels are not public API (G11)livedata-ui's public entry point exports BaseDisplayer, XWikiIcon, XWikiLivedata, displayerMixin, loadById and populateStore — and nothing else. The three panels, the per-type filter widgets and the logic class itself are internal source files, reachable only by importing past the package's declared exports.
Sibling parameters cannot be unconditionally hidden (G10)A composite widget needs the three parameters it writes to stay out of sight, and they cannot.
It re-opens G5The filter panel offers operators; the filters query string cannot store one. Reusing the reader's panel would let an author express something the macro is unable to save — the exact trap the current spec avoids by offering no operator choice.
The panels are reader-shapedThey narrow a table someone is already looking at. The properties panel is one flat list of columns with visibility and order; it has no notion of the grouped page metadata versus this object type's fields split that step 2 of the authoring flow relies on, which is the part authors were observed to need.

The one piece worth reusing first. The properties panel is much the closest fit: its reorder operation splices data.query.properties directly — already exactly this macro's properties value, in order — and neither reordering nor toggling visibility triggers an entry fetch, so it can run with no data source at all. The filter and sort panels do fetch, and the list filter additionally wants a jQuery injection and live REST autocomplete, so they need a working source even if a stub one.

Conclusion. The suggest picker stays the specification, and this stays the preferred direction for the day G10 and G11 are closed — at which point it is worth revisiting before the three widgets are written, not after. Closing G11 is a small and generally useful piece of work: export the panels and document the handful of methods they actually call, which Live Data's own unit tests already satisfy with a plain hand-written object.

Dependency and refresh model

Static structure — which parameters belong together, or are alternative ways of expressing one thing — is expressible today with @PropertyGroup and @PropertyFeature.

Value dependencies — the valid options for one parameter depend on the runtime value of another — are not expressible at all. MacroDescriptorUIFactory calls htmlDisplayerManager.display(parameterType, defaultValue, attributes, "edit"): a parameter widget receives its own type, its default value and its HTML attributes, and nothing else. It cannot read a sibling parameter, and the descriptor is cached client-side per macro id (macroService.js), so the edit template is macro-generic by construction and can never reflect the current values.

DependentDepends onKindWhat the macro editor must do
propertiesclass valueValueRepopulate the column list from that object type's fields plus doc.* metadata
filtersclass valueValueOffer the fields of the object type, and suggest each field's values from its descriptor (R3)
sortclass valueValueOffer the fields of the object type
limit (not in the first version)Live Data pageSizesConsistencyKeep the default page size within the offered page sizes

The three red edges are the whole of what R2 and UC2 promise, and none of them is declaratively expressible. All three are nonetheless implemented inside this macro, by custom suggesters that read the sibling input at query time — filters included, since its second step suggests values from the property descriptor rather than needing a typed value widget per field. See G1 for the mechanism and its caveats. Closing the gap properly is generic work — the same wall is hit by XWiki Pro's {{view-file}}, whose name parameter depends on page and which works around it with a script injected into the editor plus two MutationObservers; that workaround died silently in XWiki 18.2 when XWIKI-24088 renamed the widget wrapper classes. It therefore belongs in its own proposal, tracked by XWIKI-22850, which is no longer a prerequisite of this macro. Its shape is: a declarative @PropertyDependsOn on the descriptor, carried to the client by MacroDescriptorUIFactory, with the editor implementing the glue once (notify the dependent field with the full current parameter state, and offer a per-field server-side refresh endpoint for the cases a browser cannot recompute).

Invalidation. When class changes, properties, filters and sort cannot be carried over. Silently wiping an author's five columns is worse than refusing: the dialog asks for confirmation — a native confirmation is enough, with a translatable text — and only then applies the change. (R19) Whether "reset always" is the right policy, or whether the widget should keep what still validates against the new object type, is an open question.

Degradation

The full catalogue of messages, in the dialog and at rendering time, is in Error cases. This section keeps the shape of the degradation; that one keeps the wording. The messages are being implemented with the first version (pull request #6374).

SituationBehaviourRequirement
The dependent-parameter mechanism does not exist yetNot a situation any more: properties, filters and sort are client-side suggest pickers that need no such mechanism (G1), and no plain-input fallback mode is shipped.R18 (superseded)
The object type was deletedThe table is replaced by one legible message naming the missing object type. The macro call is untouched, so restoring the object type restores the table.R9
A field was removed from the object typeThat column is skipped, the rest of the table renders, and a warning above it names the field and the object type.R9, UC8
A field's type changed under a filterThe filter is dropped, not silently reinterpreted, and reported the same way.R9
The reader cannot view some entriesThose rows are absent and the count reflects it — no "1-5 of 24" over a set of 7 visible entries.R10
The object type has no entries yetThe table renders with its headers and an empty state, because an empty table is a correct answer.R9
Two Records tables in one pageEach keeps its own state through id; the parameter is generated unless the author sets it, and is made unique silently when two tables share one.—

A wiki page titled “Acme — project portfolio” with a warning box reading “The field Budget no longer exists in the object type Client project, so that column was skipped. Edit the macro to choose another column.”, followed by the table rendering its five remaining columns with a filter row and pagination showing entries 1 to 5 of 24.

A field removed after the macro was configured. The table keeps working, the loss is named, and the fix is one click away. (R9, UC8)

The Columns tab with an information box explaining that without the dependent-parameter mechanism no widget can read the chosen object type, followed by a plain comma-separated Columns text input and a plain query-string Filters input.

The same macro, with the dependent-parameter mechanism absent. This is the shape a fallback increment takes: the macro is complete and useful, only the assisted authoring is missing. (R18) (The mockup predates the hidden tab strip of R17, and is superseded: the first version ships no plain-input fallback.)

Technical gaps

Everything this design relies on was checked against xwiki-platform master. What is already provided is listed first; the gaps follow, each with what exists today, what it blocks, and who owns it. (R20)

Already provided

NeededProvided by
Column candidates for a class, with their types, pretty names and sortable / filterable flagsGET /xwiki/rest/liveData/sources/liveTable/properties?sourceParams.className=... — DefaultLiveDataPropertiesResource, source parameters carried by the sourceParams. prefix in AbstractLiveDataResource, descriptors built by LiveTableLiveDataPropertyStore
A filter widget and its operators, per field typeliveTableLiveDataConfiguration.json (meta.propertyTypes) mapping XClass types to filters, liveDataConfiguration.json (meta.filters) for the operators
Parameter groups, (required) / (advanced) / (deprecated) marking, submission blocked on an unset mandatory parameterMacroDescriptorUIFactory, macroEditor.js
Translated group namesTranslation key <macro>.group.<id>.name
Editing an entry in placeLive Data edit mode, 18.7.0, through the per-field editable flag of the property descriptors
A way to express a query predicate the source does not offerThe resultPage source parameter (LiveTableLiveDataEntryStore) plus #gridresultwithfilter from XWiki.LiveTableResultsMacros — how R8's exact location filter is built (G6)

G1 — a parameter widget cannot read another parameter's value

MacroDescriptorUIFactory calls htmlDisplayerManager.display(type, defaultValue, attributes, "edit"): a widget gets its own type, default and attributes, nothing else. The macro descriptor is also cached client-side per macro id (macroService.js), so the edit template is macro-generic by construction and can never reflect current values.

Today: no declared way, and nothing at all server-side. But a custom suggester covers part of it from the client, and the distinction is worth drawing precisely rather than writing the gap off as total.

The suggest widget's data source is a load(typedText, callback) function invoked on each keystroke, not a URL fixed when the edit template is rendered — see custom suggesters. It can therefore read input[name="class"] at call time and query the properties resource for whatever object type is currently selected. That is enough for properties and sort, whose options are a list of field names fetched from a service, and for filters, built as a two-step suggester: the first step suggests field names, and once = is typed the second suggests that field's values from the filter.searchURL of its property descriptor, as field=value items. What a suggester does not offer is a typed input per field (a date picker, say) nor a server-side refresh, which is exactly what the cases a browser cannot recompute would need.

Why this is not the workaround that broke. XWiki Pro's {{view-file}} injected a script and walked the editor's DOM by its widget wrapper CSS classes; XWIKI-24088 renamed them in 18.2 and it died silently. A suggester couples only to input[name="<parameter id>"], which is the contract extractFormData and toMacroCall themselves depend on to assemble any macro call: it cannot change without breaking every macro in the wiki. That is a far narrower coupling — but it is still not declared API, so anything built on it needs a functional test that fails loudly rather than silently.

Three caveats for whoever uses it, all checked against master:

  • Order. serializeArray returns a <select multiple>'s options in document order, not in the order the author picked or dragged them, so R5 is not automatic. The skin loads tom-select.complete.js, so the drag_drop plugin is available; whether it syncs item order back onto the underlying <select> has to be verified. If it does not, the widget writes its ordered value into a single hidden input named after the parameter instead of relying on the select.
  • Grouping. The page metadata versus this object type's fields split is proven only server-side, where classPicker emits <optgroup>. Options arriving from a load callback need addOptionGroup in JavaScript; Tom Select supports it, and XWiki ships no helper.
  • Invalidation. xwiki.selectize.js sets persist: true and Tom Select caches loaded options, so changing class leaves the previous type's columns in the dropdown. The widget must clear its options on the class input's change event — which is also where R19's confirmation belongs.

Blocks: nothing in the first version, since the suggesters cover all three, subject to the caveats above; only the declared, server-refreshable version of them is blocked. Fallback: none is needed (R18 is superseded).

Owner: XWIKI-22850, to be designed in its own proposal, and not a prerequisite of this macro. Shape: a declarative @PropertyDependsOn carried to the client by MacroDescriptorUIFactory, the editor notifying the dependent widget with the current parameter state, plus a per-field server-side refresh endpoint for what a browser cannot recompute.

G2 — a parameter or a group cannot be disabled

The editor knows hidden (@PropertyDisplayHidden with no value set, or a deprecated parameter with no value set) and, inside a @PropertyFeature, a disabled fieldset for the unselected radio alternatives. There is no "present but not usable yet, and here is why" state, for either a parameter or a group.

Blocks: nothing. What the editor can do is hide a group, and this design uses that: the tabs are hidden until class is set, rather than shown in a state the editor has no way to express. Recorded because an earlier revision of this page specified padlocked tabs, and a later one in-tab empty states; both were working around the same missing state. Resolved in this design; see Interaction rules.

G3 — the available layouts and page sizes are not exposed by any API

The Live Data REST module publishes sources, properties, property types and entries, and nothing else. The layouts (meta.layouts: table, cards) and the offered page sizes (meta.pagination.pageSizes: 15, 25, 50, 100) live in liveDataConfiguration.json and are reachable through $services.liveData.effectiveConfig(...) — a script service, so server-side only.

Blocks: nothing in the first version, whose layout radio group is a fixed list (table, cards) and which has no limit. It constrains the planned dynamic version: the layouts and limit widgets must be server-rendered HTMLDisplayers, which is where they were going to run anyway. A purely client-side widget would have to hardcode the two lists, and must not.

Owner: this design. A REST resource listing them would be a small, generally useful addition, but is not required here.

G4 — the macro editor has no per-parameter error channel

macroEditor.js offers exactly two things. validate() collects the mandatory parameters whose value is empty, adds has-error to the first one, focuses it, and removes the class again after one second — no message, no per-parameter text. maybeShowError replaces the whole editor body with a box errormessage when the descriptor or parameter request fails. There is nothing in between, and no server-side parameter validation before submission.

Blocks: every message in Error cases that is not "a mandatory parameter is empty". Consequence: each custom widget renders its own errors inside its own field, and the widgets may set required / pattern on their inputs, since the modal is a real form (form: true) and native constraint validation runs on submit.

Owner: this design for the widget-rendered part; a proper per-parameter error channel in the editor would be a platform improvement worth its own issue.

G5 — a filter constraint cannot carry an operator

LiveDataRendererConfiguration#getFilter builds each constraint as new LiveDataQuery.Constraint(value), so the field type's default operator is the only one applied. Several values on the same field are representable — status=Active&status=On+hold becomes two constraints on one filter.

Blocks: choosing startsWith rather than contains for a given filter. Consequence: the dialog offers no operator choice and says so, rather than offering something it cannot save. Growing an encoding for it is an open question.

G6 — the liveTable source has no exact location filter

Relevant to location, which is not in the first version.

None of the three scoping parameters the liveTable source accepts means "under this location":

  • location is a substring match. #filterLocation (templates/macros.vm) escapes %, _, [ and !, expands / to %.%, and then binds "%value%". The source's results page calls it with four arguments, so the filterWebHome branch is not taken and the clause is a single LOWER(doc.fullName) LIKE LOWER(:v) ESCAPE '!'. Searching Clients.Acme therefore also returns Archive.Clients.Acme.X (a mid-path collision) and Clients.Acmeium (a name-prefix collision). An earlier revision of this page described the two-branch doc.space / doc.fullName form; that branch exists in the macro but the liveTable source never reaches it.
  • space is an exact doc.space = :doc_space. It returns the terminal pages directly in that space and misses Clients.Acme.Child.WebHome, which an author counts as a child.
  • parent filters the deprecated doc.parent field, unrelated to the nested-page hierarchy.

Blocks: nothing, as it turns out — R8 is in scope. The source accepts a resultPage parameter, and XWiki.LiveTableResults is a 33-line Velocity block that assembles an extra where fragment plus its bindings and delegates the real work to #gridresultwithfilter, itself pulled in with . So a macro can point the source at its own results page and express the predicate it needs in about twenty lines, reusing all of the query machinery. That is what R8 does; see Mapping to Live Data.

Still missing, and out of scope: direct children only. It is a depth predicate on the same results page (doc.space = :s for the children of :s) and would be a cheap addition, but it is a different and rarer need, and this proposal deliberately exposes no choice between the two.

Costs to acknowledge: resultPage is read out of the source parameters by name, but its Java constants are package-private and the mechanism is undocumented, and XWiki.LiveTableResultsMacros is included by reference without being declared as API. Both work; neither is a contract. Two platform issues are worth filing to make them supported, and this proposal should not be the only thing standing on them.

The longer-term route: Solr already indexes space_prefix, a multi-valued field holding every ancestor space of a document, whose schema comment describes it as being "used to match descendant documents". It is populated on every indexed document and is already used as a subtree predicate by the search-exclusions query filter. What does not exist is a Solr-backed Live Data source — every shipped source is the liveTable one or a bespoke one. So the eventual clean answer is smaller than it looks, but it lives behind that source, not behind this filter. See the open questions.

G7 — editable is not a Live Data macro parameter

Not in the first version — planned future work.

LiveDataMacroParameters exposes id, properties, source, sourceParameters, sort, filters, limit, offset, layouts, showPageSizeDropdown, pageSizes and description — no editable. Editing is a per-field flag on the property descriptors (meta.propertyDescriptors[].editable), which lives in the Live Data configuration, not in the macro parameters.

Blocks: honouring editable. It would require the macro to pass a Live Data advanced configuration to LiveDataRenderer, and the renderer marks the content as trusted only when that configuration is blank or the author holds script right. Passing one would therefore downgrade the content trust of every table authored by someone without script right, and get its link displayers sanitized away. That is why the first version passes no advanced configuration, and why editable waits for a way to express it without that downgrade. The first version does not build a LiveDataConfiguration at all. This is also why Records exposes no content of its own: it owns that slot.

Note that adding an entry is separate from editing one, and is still being designed. R14 covers both; only the editing half exists today.

G8 — listing and counting XClasses is slow

The picker needs a name, a location and an entry count per candidate. Counting is one Solr facet query, but listing XClasses is known to be slow (XWIKI-16301, with XWIKI-23944 for a proper query API).

Blocks: the picker's responsiveness, not this macro's design. Owner: XClasses picker improvements.

G9 — the displayer mechanism is undocumented

@PropertyDisplayType and templates/html_displayer/<type>/edit.vm have no documentation (XINFRA-423). Three custom widgets are being asked for here, and anyone contributing one has to read MacroDescriptorUIFactory to find out how. Owner: XINFRA-423, prerequisite for outside contribution rather than for the feature.

G10 — a parameter cannot be unconditionally hidden

@PropertyDisplayHidden hides a parameter only while it has no value: maybeHideParameter and isHiddenParameterAndNoValue (macroEditor.js) reveal it as soon as one is set. There is no way for a descriptor to say "never show this parameter, whatever its value".

Blocks: the composite widget described in Alternative: reuse the Live Data panels. A single widget owning class, properties, filters and sort would write the three derived values into hidden inputs, and they would then reappear as raw fields the second time the dialog is opened — which is precisely the case UC6 is about. Blocks nothing in the specified design, where each parameter has its own widget.

Owner: a platform improvement, worth filing together with G4 since both are about what a descriptor can say to the editor.

G11 — the Live Data panels are not reusable outside a Live Data

livedata-ui exports BaseDisplayer, XWikiIcon, XWikiLivedata, displayerMixin, loadById and populateStore. The filter, sort and properties panels, the per-type filter widgets and the logic class are internal source files, reachable only by importing past the package's declared exports, and the webjar publishes no global for them either. The panels themselves are undemanding — they read a small, well-defined set of methods off an injected logic object, which Live Data's own unit tests satisfy with a plain hand-written object — but that surface is not declared, so nothing outside the module may rely on it.

Blocks: reusing the reader's own controls as the authoring widgets, i.e. the alternative evaluated under Parameter widgets. Fallback: build the three widgets, which is what this design specifies.

Owner: the Live Data module. Exporting the panels and documenting that logic surface — plus shipping an in-memory LiveDataSource, since the only implementation today is the REST-backed one — is small, and would pay off well beyond this macro.

G12 — the description's DOM id collapses when the table has no id

LivedataLayout builds the description's element id as `${logic.data.id}-description` and points the layout at it with aria-describedby. When no id is set — which is the default, since the parameter is optional — data.id is undefined and the id renders literally as undefined-description. Verified on 18.8.0-SNAPSHOT.

Blocks: nothing for a single table, where the reference still resolves. Breaks two tables on one page without explicit ids: both descriptions get the same DOM id, so the first one wins and the second table is described by the first table's text. It affects the {{liveData}} macro equally, so it is not specific to this design.

Consequence here: id is not merely about keeping each table's reader state apart, as its description says; without it the accessible description is ambiguous. The macro therefore always sets an id when the author leaves it empty, generated through the document's identifier generator so that it is unique (the smaller change, done in the first version); Live Data building ids from an undefined value remains a Live Data issue.

Owner: a Live Data issue for the id building, and this design for generating an id.

Interaction rules

Every state of every control, so that the implementation does not have to guess and the mockups can be checked against something. A control that cannot be used yet is hidden rather than "waiting" (G2).

Two framework constraints bound this table rather than following from it. Tabs cannot be disabled, only hidden (G2), which is why the tabs are hidden until the object type is set (R17). And the tab order is not freely choosable: it comes out right only because every optional parameter carries an explicit @PropertyGroup, which leaves the framework's default "Optional parameters" group childless so that it is never added to the strip — see Authoring flow. An implementation that drops one @PropertyGroup will silently get a different tab order, with that default group first.

ControlStateCondition
Object type pickerEditable, focused on openAlways. It is the only mandatory parameter.
Object type pickerShows a chip (title, location, entry count) with Change and clear (the improved picker, out of scope for the first version)An object type is set
Object type picker suggestionsListed, keyboard-navigableAt least one character typed, or the field focused with an empty query (the picker's own design, XClassPickerImprovements)
Location picker (not in the first version)Empty, with a hint that the whole wiki is listedNo location set — the default (R8). It sits with class, above the tab strip, so it stays available while the tabs are hidden
Location picker (not in the first version)Shows the picked page, with clearA location is set
Location picker (not in the first version)Warns in place: "This page has nothing stored under it"A terminal page was picked. A terminal page has no space of its own, so nothing can be nested below it; the table would list at most that one entry
SubmitDisabled while class is emptyThe editor blocks submission on an empty mandatory parameter, flags the field, and focuses it
The tab stripHidden entirelyclass empty. Every tab is derived from the object type, so none of them has anything to offer yet (R17)
The tab stripShown, on the Columns tabclass set
Columns, Filter & sort, Display, Advanced tabsAll reachable once shownA tab cannot be disabled, only hidden (G2)
Columns pickerPopulated, with the candidates grouped as Page metadata and <object type> fieldsclass set and the properties request succeeded
Columns → candidatesContain the page metadata group and the object type's fieldsclass set
Columns → chosen itemsOrdered, drag-reorderable, each removableAt least one column chosen
Columns → chosen itemsEmpty, with the placeholder "Title and every field"properties empty; not an error, and Submit stays available since the default is the title followed by all fields
Filters → field suggestionsOffer every field of the object type, not only the displayed columnsclass set
Filters → operatorNot shown; the field type's default appliesAlways (G5)
Filters → value suggestionsRead from the filter.searchURL of the field's descriptor; free text allowed; none for a field without one= typed after a field
Filters → chosen itemsOne per field=value, each removableAt least one filter chosen
Sort → suggestionsTwo per field, ascending and descending, among all the fields of the object type; a field already used disappears from bothclass set
Sort → chosen itemsOrdered, each removableAt least one criterion chosen
LayoutTwo radio buttons, table (the default) and cardsAlways
Entries per page (not in the first version)Offers the page sizes from the Live Data configurationAlways (G3)
Let readers add and edit entries (not in the first version)Available; the hint states that per-entry rights still applyAlways. Adding entries lights up only where the platform supports it (G7)
Advanced tabAlways reachable, its parameters marked (advanced)Advanced parameters are marked, not hidden

Consequences of a change

The author does thisWhat happens
Picks an object type for the first timeThe tab strip appears and the dependent tabs populate. Nothing is lost, because nothing was set.
Changes the object typeproperties, filters and sort cannot be carried over. A native confirmation, with a translatable text, warns that the columns, the filters and the sort will be reset, and only then applies the change. Cancelling leaves everything untouched. (R19, UC11)
Clears the object typeSame confirmation, then the tab strip is hidden again, the dialog is back to its opening state and Submit becomes unavailable.
Removes a column that a filter or the sort usesNothing else changes: the candidates for filters and sort are all the fields of the object type, not only the displayed columns, so the filter or the sort stays, whether or not its column is displayed.
Sets or clears the location (not in the first version)Only location changes. It scopes the rows and has no bearing on the columns, the filters or the sort, so nothing else is invalidated and no confirmation is needed.
Reorders the displayed columnsOnly properties changes; filters and sort are unaffected, since they name columns rather than positions.
Reopens the macro months laterThe dialog opens on the saved configuration, not on defaults. Reopening and submitting without an edit produces a byte-identical macro call. (R7, UC6)

Error cases

Split into what the system failed to do and what the author got wrong, because they need different treatments: a technical failure must never lose the author's work, and a validation message must be actionable. Each message below is a translation key of the macro's own bundle, written as a full sentence with parameters (R16, i18n).

Technical errors, in the dialog

The Columns tab with a red error box reading “The fields of Client project could not be loaded. The columns already chosen are kept and will be saved as they are.” next to a Try again button, above a dimmed transfer list whose Displayed panel still holds Title and Client.

Note: this mockup is superseded by the pull request screenshots where it contradicts the implementation: a failure to load the fields of the object type currently shows an empty list, and the messages of this section are being implemented with the first version.

What failedWhat the author seesWhat happens to the configuration
The properties request fails (network, server error)In the Columns and Filter & sort fields: The fields of {0} could not be loaded. plus a Try again buttonKept. Already-chosen columns and filters stay, and are saved as they are. This is the case shown above.
The chosen object type no longer exists when the dialog is reopenedOn the picker: The object type {0} no longer exists. Pick another one to continue. The picker opens ready to search.Kept until the author picks a replacement, so that reopening a page does not silently wipe a configuration.
The author may no longer view the chosen object typeSame message as above — a picker must not reveal, by wording, that something exists but is out of reach (R10)Kept.
The macro descriptor itself fails to loadThe editor's own whole-body error box, which is all the framework offers here (G4)Not applicable — the dialog never opened.

Author input, in the dialog

SituationMessageWhere
class empty on submitThe editor's own behaviour: the field is flagged and focused, with no message (G4). The picker adds Pick an object type to insert the table. under itselfUnder the object type field
A field name is unknown (only if a plain-input fallback is ever shipped; R18 is superseded)"{0}" is not a field of {1}. Known fields: {2}.Under the Columns input, which is also outlined
A filter names a column that is not displayed (only if a plain-input fallback is ever shipped)The filter on "{0}" is ignored: that column is not displayed. A warning, not an error: the macro still rendersUnder the Filters input
A filter value matches no suggestionAccepted as free text: values that cannot be enumerated must stay possible, so the dialog does not validate it against the field type—
limit is not a positive integer, or is outside the offered page sizes (not in the first version)Enter a number of entries between {0} and {1}. The widget also sets min / max, so the browser's own validation fires on submit (G4)Under the field
Two Records tables on one page share an explicit idNo message: the identifier is made unique silently, and a collision becomes <id>-1—
The last layout is uncheckedNot applicable: the layout is a radio group, so one layout is always selected—

At rendering time

The macro is executed long after it was configured, by readers who cannot fix it, so the rule is: render as much of the table as still makes sense, and explain the rest above it. Rendering-time messages are informational for a reader and actionable for someone with edit rights; none of them is a stack trace, and none of them replaces the whole table unless the table cannot exist at all.

SituationRenderingMessage
The object type was deletedNo tableThis table lists entries of the object type {0}, which no longer exists.
A displayed field no longer existsTable, minus that columnThe field {0} no longer exists in the object type {1}, so that column was skipped. (UC8, and the mockup in Degradation)
A field's type changed and a filter no longer appliesTable, without that filterThe filter on {0} was ignored because that field changed type.
A field is neither sortable nor filterable but is sorted onTable, unsorted on that column{0} cannot be used for sorting, so the table is sorted on {1}.
The reader may not view some matching entriesTable, minus those rows, count adjustedNo message: a reader must not learn that entries exist that they may not see (R10)
The object type exists but holds no entryEmpty table with its headersNo entry yet. Plus, for a reader with edit rights and editable on, the affordance to add the first one
The location no longer exists, or the reader may not view itEmpty table with its headersNo entry yet. No message about the location: a reader must not learn from an error that a page exists which they may not see (R10). The author sees the broken reference in the dialog instead
The Live Data source itself failsNo tableThe table could not be loaded. Try reloading the page. The technical detail goes to the log, not to the reader

Mapping to Live Data

The liveTable source is enough for this proposal.

What the macro producesWhere it goes
classThe source's class parameter; also the key the property descriptors are fetched for
propertiesLive Data properties, order preserved
filtersParsed by LiveDataRendererConfiguration#getFilters as a query string
sortParsed by LiveDataRendererConfiguration#getSortEntry as a prop:asc|desc list
layouts, description, idThe Live Data macro parameters of the same name (id is always set, and made unique). limit is not in the first version: Live Data's own default applies
editableNot in the first version. It would map onto the per-field editable flag of the property descriptors, in the Live Data configuration — there is no editable macro parameter (G7)
locationNot in the first version. It would be a source parameter of the results page this macro ships — not the source's own location parameter, whose semantics are different (R8, G6)
Column headersNot a parameter: the field's own translated pretty name, which LiveTableLiveDataPropertyStore already puts on each property descriptor. For the doc.* columns the macro passes a translationPrefix source parameter of its own, exactly as the documents macro passes platform.index. (see Internationalisation)

The macro does not build a LiveDataConfiguration. It maps its parameters onto the Live Data renderer's parameters and calls LiveDataRenderer with no advanced configuration, because the renderer marks the content as trusted only when the advanced configuration is blank or the author holds script right: passing one would downgrade the content trust of every table authored by someone without script right, and get its HTML displayers sanitized away (G7). The macro therefore sticks to what its parameters can express. That is also why Records exposes no content of its own: it owns that slot.

Known limitation. The liveTable source returns one row per page, showing the first object of the class on that page. A page that holds N objects of the class therefore shows N identical rows, all showing that first object (XWIKI-23074).

The macro's parameters and Live Data's advanced JSON configuration are not interchangeable, and must not both be written: the merge is merge(advancedConfig, basicConfig) with the parameter-derived configuration on the right, and JSONMerge drops left-hand array items that carry no id — which filter and sort entries do not. Hence "no macro content" above.

The results page (not in the first version — planned future work). R8's exact location filter is not expressible through the source's parameters (G6), so the macro passes resultPage and ships its own results page. It is a copy of the thirty-odd lines of XWiki.LiveTableResults with the location clause replaced:

AND (doc.space = :locSpace OR doc.space LIKE :locPrefix ESCAPE '!')

Three details decide whether this is correct rather than merely short:

  • doc.space holds the local space reference, so the picked page must be resolved to a space: a nested page Clients.Acme.WebHome scopes to Clients.Acme, while a terminal page has no space of its own and can have nothing below it — the dialog says so rather than silently returning one row.
  • %, _, [ and ! must be escaped with ! in the prefix binding, exactly as #filterLocation does, or a space whose name contains an underscore silently widens the match.
  • Everything else — the class join, the column selection, the filters, the sort, the view-rights check and the count — stays in #gridresultwithfilter. The page adds one predicate and delegates the rest, which is what keeps it maintainable and R10 intact.

A Solr-backed source would remove the liveTable source's scaling limits, but only if XWiki's Solr schema indexes arbitrary XObject property values in a filterable and sortable way. That is a question, not a plan — see the open questions — and the XClass -> Live Data descriptor mapping should be extracted out of xwiki-platform-livedata-livetable into a reusable component before anyone tries.

Dependencies on other proposals

ProposalWhat this macro needs from itWithout it
XClasses picker improvementsA picker an author can use: search on a human-readable name, location, entry count, technical types hidden by defaultclass falls back to the existing developer-facing picker, which is what the first version ships. The macro works; UC1 does not. Improving that picker is out of scope for this page.
Dependent macro parameter widgets (XWIKI-22850, proposal to be written)A declared way for a widget to read, and react to, the value of a sibling parameterNot a prerequisite. properties, filters and sort are client-side suggest pickers that read input[name="class"] when asked for suggestions (G1). The declared mechanism would replace that coupling, not enable the feature.
Vocabulary (forum)The final term for XClassThe UI ships with object type, and is renamed later if the wider discussion settles on another term. Either way this design is unchanged.

Neither dependency blocks the first version: it uses the existing class picker, and needs no dependent-parameter mechanism.

Delivery order

# The object type picker: the first version reuses the existing class picker. A dedicated HTMLDisplayer showing title, location and entry count is the subject of the separate picker proposal; the entry count is one Solr facet query.
# The macro itself, with its four-tab dialog and its pickers — the first version (XWIKI-24831, pull request #6374). It ships with no plain-input fallback.
# The results page carrying the exact location filter (and location itself, not in the first version), in a new -ui XAR submodule, plus the two platform issues that make resultPage and XWiki.LiveTableResultsMacros supported rather than merely working (G6). Small, and independent of everything above it.
# The visibility rule for object types (which are offered to a non-advanced user, and how a technical one is recognised): blocked on the forum discussion, not on code, and must not change the existing classPicker behaviour.
# The dependent-parameter mechanism (its own proposal). No longer a prerequisite of the next two, which are built as client-side suggesters.
# The ordered column picker, fed by the Live Data properties REST resource (shipped with 2).
# The filter and sort widgets, derived from the field types (shipped with 2).
# Extracting the XClass to Live Data descriptor mapping into a reusable component.
# The App Within Minutes generated home page switched over to this macro.

Also planned future work, not in the list above: limit with its page-size select, editable, and building the layout list dynamically from the registered layouts.

Documenting the @PropertyDisplayType / html_displayer mechanism (XINFRA-423) is a prerequisite for anyone else contributing 1, 6 or 7.

Rationale and rejected alternatives

  • The documents macro is focused on listing pages and must stay specialized for that. This macro specializes in displaying structured entries; the UI leads with entries and mentions pages only where they matter (R15).
  • A separate macro is what makes it possible to fine-tune the usability for non-advanced users. A generic macro cannot both keep every Live Data capability and validate everything against one object type.
  • Listing structured data is a core part of XWiki: while the implementation stays modular, it belongs in xwiki-platform and should be available by default.

Chosen: option 2.1. Options 1 and 3 are rejected, and option 2.2 is deferred to a generic macro-preview proposal, as the forum discussion concluded.

Each other option below is kept with the mockups it was discussed with. The rendering layer is identical in all of them; only the authoring differs.

Option 1 — no improvements

The Records macro dialog with empty Class, Properties and Location text fields.

The same dialog with an invalid class reference typed in and a red error banner reading “No class named Client.Code.ProjectClass”.

A rewrite of the Live Data macro with a fine-tuned vocabulary and none of its generic aspects (no source choice), but no custom displayers and no conditional display. The class is free text, so the author must already know the reference, and properties stays a comma-separated string filled from memory. Sub-options: no validation at all, everything passed through to Live Data (1.1), or server-side parameter validation (1.2).

Cheapest, and rejected: it does not serve the audience the proposal exists for. It does not survive as a fallback shape of option 2.1 either, since the first version ships no plain-input fallback (R18 is superseded).

Option 2.2 — custom displayers and a custom preview

The Records macro dialog with the parameter form on the left and a preview pane on the right rendering the resulting Live Data table.

The same dialog with nine columns selected; the preview pane shows a horizontal scrollbar and a warning that the table is wider than the pane.

Option 2.1 plus a live preview of the table in the dialog, so the author sees the effect of each parameter change while editing (R13). It is a real usability win, and it is also the natural home for filter configuration by direct manipulation — filter and sort the real table, keep the state.

Deferred to a generic macro-preview proposal (to be written; this page will link it once it exists), not rejected on principle: a macro preview is something we want generically, and an implementation private to this macro would be wasted work. The second mockup shows a problem the generic design has to answer anyway — the preview pane is narrower than the page the table will land on, so it scrolls where the real page will not, and the preview can mislead about the very thing it exists to show.

That rejection is narrower than it looks, and worth re-reading next to Parameter widgets. "An ad-hoc preview is wasted work" holds for a pane that only shows the result. It does not hold for a mounted Live Data that is the configuration widget — the author filters and sorts the real table, and the resulting query is what gets saved. Then the table is not a preview at all, it is the control, and the generic-preview argument stops applying. What blocks that is G10 and G11, not this rationale.

Option 3 — in-place edition

A wiki page in the editor containing an empty Records block that asks “What kind of data should this table list?” with an application search field and suggestions.

The same page with the table configured, a toolbar of parameter controls above it, and an “Add a column” menu open from the table header.

No macro modal at all: the table's own toolbar and headers drive the parameters, each interaction writing one back and refreshing the render. This is the closest to what Notion offers, and the forum discussion liked the UX.

Rejected: experimental, the most expensive option, and hard to generalize — every macro wanting the same look and feel would need its own development. An investigation of what it would actually take found the obstacles to be concrete rather than theoretical: Live Data's JavaScript is stripped by the standalone WYSIWYG's script filter, xwiki:dom:updated is fired with the parent page's jQuery while the iframe's listeners use the iframe's jQuery, the config-trust carrier is a <script> element the editor removes, and every parameter change re-renders the macro, destroying and recreating the Live Data application. It would only be worth it if listing structured data were judged to deserve special treatment over every other macro. A mix of 2.x and 3 (basic manipulation in place, the full set of options in the modal) was suggested in the discussion and is the interesting long-term shape, but it is strictly more work than either.

A wizard instead of tabs

Explored because this macro's parameters form an unusually clean chain — class decides properties, which decides filters and sort, and only Display is independent — and because a wizard would make that order visible instead of having to explain it. It would also delete the entire category of "waiting" empty states, which exist only because tabs are all reachable at once (G2). The framework is not hostile to the idea either: macroWizard.js already chains modals (selectMacro then editMacro, looping back when the author changes macro), and macroEditor.js already varies its own chrome, dropping the tab strip when there is only one group.

Rejected, for three reasons, of which the second is decisive:

  • There is no per-macro opt-in. The groups become tabs in displayOptionalNodes, and macro-editor-modal is shared by every macro in the wiki. Rendering this one as a wizard is a platform-wide change to a shared control, and no descriptor flag exists to ask for it.
  • Step order is exactly what a Java macro cannot control. In a tab strip the order is cosmetic; in a wizard it is the whole point. But MacroDescriptorUIFactory pins the default "Optional parameters" group at order 0 and DefaultBeanDescriptor drops any @PropertyOrder that is not > 0, so no Java macro can put a step before it. A wizard whose first step is Optional parameters is worse than the tabs it replaced. The workaround this design already relies on — leaving the default group childless — is enough for tabs, where a wrong order is untidy, and too fragile for a wizard, where a wrong order is broken.
  • It penalises the case the design is explicit about. UC6 and R7 are about reopening the macro months later to change one column. A wizard turns a one-field edit into a walk through every step, and that is the interaction an author repeats.

Recorded rather than dropped, because the seam for it already exists: macroWizard.js distinguishes insertMacroWizard from editMacroWizard, so "wizard on first insertion, tabs when editing" would need no new mechanism — only the two platform changes above, and a reason to prefer it that is stronger than the ones found here.

Considerations

CharacteristicAssessment
AccessibilityR12 for the rendered table, R16 for the dialog. Two things found while implementing: the description is a visible paragraph wired with aria-describedby, so it is the accessible description and not the name, and the table itself has no accessible name — worth giving it one. And the description's DOM id collapses without id (G12). The dialog is the harder half: the column picker needs a keyboard path for both selection and reordering, not only drag and drop, and a disabled parameter must say why in text rather than only by being dimmed.
UsabilityThe whole point of the option. Two specific risks: the multi-value picker is a pattern that may test badly with non-technical users when it is the only affordance, and "each field takes one filter" is a limitation an author meets by surprise. Both are worth a UX review before implementation (cc the UX team).
InternationalisationEvery label, description and error message is a translation key of the macro's own bundle. Field headers come from the field's translated pretty name, which PropertyClass#getTranslatedPrettyName already resolves from the class's own bundle and which falls back to the pretty name stored in the class editor; doc.* headers come from a prefix the macro sets internally. There is deliberately no translationPrefix parameter: it would override the field names the object type's author chose, which is the opposite of what an author of this macro wants, and no other macro in the platform exposes it. Messages must be built from full sentences with parameters, never concatenated fragments, and the vocabulary decision changes only the values of these keys. The macro name and description are translated too, since the quick action searches them.
Backwards compatibilityNothing to break: a new macro, no change to documents or to the Live Data macro, and the picker work is required to keep classPicker's current behaviour. The one forward-compatibility constraint is the filters encoding: growing it to express per-constraint operators must stay readable by the current parser.
SecurityR10. View rights are enforced by the liveTable source, but the dialog is a second surface: the picker must only suggest object types the author may view, and the entry counts it shows must not leak the existence of entries the author cannot see.
PerformanceR11. Pagination comes from Live Data. The new costs are in the dialog: the entry count per suggestion in the picker, and the property descriptor request on every object type change. Both are per-interaction, cacheable, and must not scan. Listing XClasses is currently slow (XWIKI-16301), which the picker proposal has to address.
ReliabilityR9, and the degradation table above. The rule is that a broken configuration degrades to a legible message plus as much of the table as still works, never to a stack trace and never to silence.
MaintainabilityThe risk of the option: three custom widgets to keep working across widget-library changes. This is exactly how {{view-file}} broke in 18.2. The mitigation is that all three are ordinary HTMLDisplayers with no DOM archaeology in the editor — their suggester route (G1) couples only to the parameter-named inputs the editor cannot rename, not to presentation classes, and that the column and filter widgets are fed by an existing REST resource rather than a private query. Note that these three widgets duplicate controls Live Data already has, and that reusing those instead would remove the duplication and G1 at once — it is blocked only by G10 and G11, and is the direction to revisit before the widgets are written rather than after (see Parameter widgets). The results page R8 needs is a second maintenance item, kept small on purpose: one predicate, everything else delegated.
SustainabilityNo new query path: the columns and filters come from Live Data descriptors, the entry counts from Solr facets. The rendered table stays paginated, so the page weight is bounded by Live Data's page size and not by the size of the object type.
DocumentationThe macro needs a reference page and, more importantly, a task-shaped page (list your structured data in a page) since the audience is non-advanced users. One thing must be documented explicitly once location exists: location here means this page and its descendants, exactly, whereas the documents macro's parameter of the same name is a substring match. Same word, two behaviours, across two macros an author may well use on the same page. The @PropertyDisplayType / html_displayer mechanism needs documenting for the widgets to be contributable (XINFRA-423).

Open questions

  • The macro name. Closed, see Naming. The macro id is records, because it is syntax and cannot be changed after the first release, the name is Records and the description is Displays a collection of entries of the same object type, as a table readers can sort and filter. records was found too generic on the thread, and data collection, entries and collection were each suggested; the name and description remain translation keys that can still be adjusted in any release.
  • The wider vocabulary, pending its own discussion. This macro uses object type; this design depends on the outcome of the wider discussion only for its strings.
  • Should filters grow an encoding for per-constraint operators? Today the field type's default operator is the only one representable, so the dialog offers no operator choice. The alternative is an encoding (e.g. status=startsWith:Act) that the current parser would have to keep reading.
  • Is it acceptable that location means something different here than in the documents macro? This proposal picks exact scoping — this page and its descendants — because an author who cannot debug a query should not have to reason about why Clients.Acmeium turned up. The documents macro has taught users a substring match under the same parameter name (and says so in its own description). Two behaviours behind one word is a real cost; the alternatives are to inherit the substring match here, or to fix documents too, which is a compatibility question of its own.
  • Should the default column list open with doc.title? Resolved: it does, and the first version builds it that way (doc.title, then every field of the object type in class order). It does, because a table of nothing but field values gives a reader no way to tell one entry from another and no way to reach the page an entry lives in — and every mockup on this page shows an identifying first column. The cost is that the documented default is no longer purely "the object type's fields". Naming any column replaces the list, so an author who wants the fields alone can still say so.
  • Is "reset always" the right invalidation policy when the object type changes, or should the widget keep the columns and filters that still validate against the new type?
  • Should the row set be allowed to be several object types, or an object type plus a location? Confluence's label model is naturally multi-valued; ours is not.
  • Does XWiki's Solr schema already index arbitrary XObject property values in a filterable and sortable way? This decides whether a Solr-backed source is a small change or a large one. Note that the location half of the question is already answered: space_prefix exists, is populated on every indexed document and is documented for matching descendants — what is missing is a Solr-backed Live Data source to use it from (G6).
  • Which object types is a non-advanced user offered? Owned by the picker discussion, listed here because this macro is its first consumer.

Prior art and references


 

Get Connected