Views¶
Views in eQual's equal-ui define how an entity is loaded, presented, filtered, edited, and acted upon in the user interface. On the client side, a view is instantiated by the View class with an entity, a type, a name, an initial domain, a mode, a purpose, a language, and an optional configuration object.
This page documents the behavior actually consumed by equal-ui/src/View.ts, equal-ui/src/Model.ts, the Layout* classes, and WidgetFactory.
Lifecycle¶
When a view is created, View:
- initializes the default configuration;
- loads the entity translations;
- loads the model schema;
- loads the view schema identified by
{type}.{name}; - extracts the fields required by the layout and dynamic domains;
- prepares filters, exports, and header actions;
- initializes the layout matching the view type;
- loads the model through
Model.refresh().
The model calls the configured controller with the following parameters:
{
"get": "model_collect",
"entity": "package\\Model",
"fields": ["..."],
"domain": ["..."],
"params": {"lang": "en", "order": "id", "sort": "asc", "start": 0, "limit": 25}
}
The parameters returned by getParams() always include lang, order, sort, start, and limit.
View Types¶
| Type | Client Layout | Description |
|---|---|---|
list |
LayoutList |
Paginated table with sorting, filters, selection, row actions, global actions, and grouping. |
form |
LayoutForm |
Single-object form, in view or edit mode. |
search |
LayoutSearch |
Advanced-search form used as an embedded view. |
chart |
LayoutChart |
Chart.js chart or grid depending on the mode. |
dashboard |
LayoutDashboard |
Grid of widgets embedding other views. |
| other | Layout |
Base layout without specific rendering behavior. |
Widgets only support two internal layouts: list and form. Any view type other than list is treated as form for widget rendering.
Modes and Purposes¶
mode¶
| Type | Recognized Modes | Fallback |
|---|---|---|
list |
view, edit |
view |
form |
view, edit |
view |
chart |
chart, grid |
chart |
| other types | not normalized by View |
received value |
purpose¶
purpose describes the usage context and changes buttons, selection, and actions:
| Purpose | Main Effect |
|---|---|
view |
Normal browsing. Selection actions and exports are available according to configuration. |
select |
The list allows selecting one or more objects, then closes the context with {selection, objects}. |
add |
Same as select, but the button is labelled as an add action. |
create |
Edit form for a new object. Model-level readonly fields are rendered as editable. |
update |
Edit form for an existing object. |
widget |
View embedded in a relational widget or dashboard. Actions and selection are restricted. |
Root Properties¶
| Property | Type | Default / Behavior |
|---|---|---|
controller |
string |
Collection controller. Client default: model_collect. If the view schema defines controller, it replaces the config value. |
mode |
string |
Initial view mode, normalized according to the type. |
order |
string |
Sort field. Default id. The view schema value is applied only if config has not already changed the default value. |
sort |
string |
Sort direction. Default asc. |
start |
number |
Pagination offset. Default 0. |
limit |
number |
Page size. Default 25. |
group_by |
array |
List grouping definitions. Default []. |
layout |
object |
Rendering structure, dependent on the view type. |
filters |
array or false |
If it is an array, each filter is indexed by id. If the property exists but is not an array, header filters are disabled. |
actions |
array |
Root-level domain actions. Their rendering depends on the layout. |
routes |
array |
Contextual links, currently used as inline actions in lists. |
exports |
array |
Exports added or replacing defaults indexed by the same id. |
operations |
object |
Aggregates displayed on lists. |
on_empty |
object |
For empty lists: redirects through context or displays message. |
on_error.missing.context |
object |
For forms without an object: closes open contexts and opens this context. |
name and description may exist in schemas, but the equal-ui client mostly relies on the {type}.{name} identifier, translations, and the properties above.
Configuration Priority¶
The constructor configuration and the view schema are partially merged:
configinitializes runtime values (mode,controller,order,sort,start,limit,group_by,header, etc.).view_schema.headeris merged intoconfig.header, thenconfig.headerkeeps priority for matching keys.view_schema.controlleralways replaces the runtime controller.view_schema.order,sort,start,limit, andgroup_byonly replace the runtime value if it still equals the client default.view_schema.header.actionsfeedscustom_actions, thenconfig.header.actionsoverrides these actions.
Domains and Loaded Fields¶
The effective domain of a view is:
initial context domain + clauses/domains from applied filters
The result is then parsed with the current user (user.*). References to env.*, parent.*, and object.* are also used in other domain evaluations, especially visibility and relational widgets.
View.loadViewFields() adds to the projection fields explicitly present in the layout, but also fields referenced by:
visibleanddomainin the layout;routes[].context.domain;routes[].visible;actions[].visible;header.actions.*[].visible,domain,view,edit,create,update.
Model.getFieldsProjection() then applies these rules:
- scalar field: the field is loaded directly;
many2one:field.nameis always loaded, together with subfields requested throughfield.subfieldorwidget.fields;one2manyandmany2many: not loaded in the main collection, because they are rendered by sub-views;statusandorderare added if present in the model schema.
Header¶
header controls standard actions, filters, exports, pagination, and compact layout.
| Property | Type | Behavior |
|---|---|---|
actions |
object or false |
Configures standard actions. If false, header actions are disabled. |
selection |
object or false |
Configures actions applicable to a selection. If false, selection is disabled and mode is forced to view for the affected lists. |
filters.quicksearch |
boolean |
Shows or hides quick search on name. Default: enabled when filters are visible. |
layout |
full or inline |
In inline, actions and controls are rendered compactly, especially for relational widgets. |
pagination |
full, compact, or none |
Controls list paginator rendering. full displays the complete paginator, compact displays a lighter paginator, and none hides the paginator. |
advanced_search |
object or false |
If false, disables advanced search. Otherwise, it is available for controllers different from model_collect and core_model_collect. |
advanced_search.open |
boolean |
Opens the advanced-search panel on load. |
advanced_search.submit |
auto or manual |
In advanced search, applies changes automatically or through a button. |
exports |
false |
Disables the list export menu when false. |
Example:
{
"header": {
"layout": "inline",
"pagination": "compact",
"filters": {
"quicksearch": false
},
"advanced_search": {
"open": true,
"submit": "manual"
},
"actions": {
"ACTION.CREATE": false
}
}
}
Header Actions¶
A header action can be:
trueorfalse;- a descriptor object;
- an array of descriptors.
An empty array is interpreted as available by isActionEnabled(), but some actions, notably ACTION.SAVE, also check that the array contains at least one variant.
Descriptor¶
| Property | Type | Description |
|---|---|---|
id |
string |
Variant identifier (SAVE_AND_CLOSE, etc.) or selection-action identifier. |
label |
string |
Label used for custom actions. |
icon |
string |
Material icon. |
description |
string |
Text used in controller-action dialogs. |
controller |
string |
Controller called by a custom action or selection action. |
params |
object |
Parameters injected into the controller. Values can reference object.*, user.*, and parent.*. |
confirm |
boolean |
Shows a confirmation or forces a parameter dialog before execution. |
visible |
boolean or Domain |
Global visibility of the action. |
view, edit, create, update |
boolean or Domain |
Availability by mode/purpose depending on isActionEnabled() calls. |
domain |
Domain |
Additional domain used by create/selection actions or to load required fields. |
view_id |
string |
Target view in type.name format for several create/save actions. |
access.groups |
string[] |
Taken into account for root-level form actions. |
Important: for ACTION.CREATE and save variants, the code reads view_id, not view. In many2one widgets, header.view can also be used to open the related object.
Lists¶
Header actions wired for lists:
| Action | Behavior |
|---|---|
ACTION.CREATE |
Opens a form context in mode: edit, purpose: create. Can use view_id and merge an additional domain. |
ACTION.CREATE_INLINE |
Creates a draft object, inserts it as the first row, then switches the row to inline edit. Available in non-view mode, especially with header.layout: inline, or when explicitly configured. |
ACTION.SELECT |
Available for purpose: select or add; closes the context with selected objects. |
In a normal list, ACTION.CREATE is available by default if purpose is not widget, or if the widget list is in edit mode.
Forms¶
Header actions wired for forms:
| Action | Behavior |
|---|---|
ACTION.EDIT |
In view mode, opens the same form in mode: edit, purpose: update, preserving selected sections. |
ACTION.SAVE |
In edit mode, saves through model_update or through the variant controller. |
ACTION.CANCEL |
Closes the context after confirmation if changes exist. |
Save variants:
| Variant | Behavior |
|---|---|
SAVE_AND_CLOSE |
Saves, marks the context as changed, closes the context, and relays {selection, objects}. |
SAVE_AND_VIEW |
Saves, then closes the parent context when possible; otherwise opens the target view in view mode. |
SAVE_AND_EDIT |
Saves, then reopens the target view in edit mode. |
SAVE_AND_CONTINUE |
Saves, replaces the domain with id = object_id, displays a snackbar, and reloads the view. |
The client default for ACTION.SAVE is [{"id": "SAVE_AND_CLOSE"}].
Root-Level Actions¶
Root-level actions are not the same as header.actions.
Forms¶
In forms, root-level actions are displayed only in view mode:
- one visible action: direct button;
- several visible actions:
Actionsdropdown menu.
Visibility combines:
visible, evaluated as a boolean, JSON string, or domain;access.groups, when defined.
Lists¶
In lists:
- a root-level action without
inline: trueis displayed in the view action area, except whenpurpose === "widget"; - an action with
inline: trueis rendered on each row; - inline visibility is evaluated per row through
visible.
Execution¶
decorateActionButton() supports three families:
| Family | Trigger |
|---|---|
callback |
JavaScript function called with the current object. |
component |
Emits window.dispatchEvent(new CustomEvent("App:open-component", ...)). |
controller |
eQual controller call with prior announcement, optional dialog, and refresh. |
For a controller action:
paramsis initialized when absent;params.paramsreceives the view runtime parameters;- if a current object has an
id,id: "object.id"is added when absent; - otherwise,
domainreceives the serialized current domain; - if the view is a widget, the parent object is loaded and made available through
parent.*; - references are resolved;
- the controller is called with
announce: true; - required parameters or parameters requested by
confirmare displayed in a dialog; performAction()executes the action.
Responses handled by performAction():
| Response | Behavior |
|---|---|
non-JSON content-type |
Download through file-saver. |
HTTP 202 |
"Action request sent" snackbar. |
HTTP 205 |
Marks the context as changed and closes it. |
HTTP 302 with Location |
Browser redirect. |
| other success statuses | Refreshes the main view of the current context. |
| error | displayErrorFeedback() using the controller translation. |
Selection¶
header.selection configures actions available when one or more rows are selected.
| Property | Type | Behavior |
|---|---|---|
default |
boolean |
If absent or true, default actions are included. If false, only provided actions are used. |
actions |
array |
Actions added, replaced, or hidden according to their id. |
Default actions:
| ID | Effect |
|---|---|
ACTION.EDIT_INLINE |
Switches selected rows to inline edit. |
ACTION.EDIT_BULK |
Opens bulk assignment, but the action is marked invisible by default. |
ACTION.EDIT |
Opens the first selected object in an edit form. |
ACTION.CLONE |
Clones selected objects, then reloads the view. |
ACTION.ARCHIVE |
Asks for confirmation, then archives selected objects. |
ACTION.DELETE |
Asks for confirmation, then deletes selected objects. |
A custom action with a controller receives at least:
{
"entity": "package\\Model",
"ids": [1, 2, 3],
"id": 0,
"lang": "en"
}
It then follows the same announcement, dialog, and execution flow as root-level actions.
Example:
{
"header": {
"selection": {
"default": false,
"actions": [
{"id": "ACTION.CLONE"},
{
"id": "header.selection.actions.mark_ignored",
"label": "Mark as ignored",
"icon": "block",
"controller": "lodging_sale_booking_bankstatementline_bulk-ignore",
"confirm": true
}
]
}
}
}
Filters¶
A view filter is indexed by its id.
| Property | Type | Description |
|---|---|---|
id |
string |
Filter identifier. |
label |
string |
Label. |
description |
string |
Text displayed in the filter menu. |
clause |
Domain |
Domain added to applied filters. |
domain |
Domain |
Alternative to clause, also merged into the current domain. |
Applied filters are cumulative. The menu also allows adding a custom filter through decorateCustomFilterDialog().
Quick search dynamically creates a filter_search_on_name filter:
["name", "ilike", "%value%"]ifname.result_type === "string";- otherwise
["name", "=", "value"].
Exports¶
Defaults:
- list:
export.pdfthroughmodel_export-pdfandexport.xlsthroughmodel_export-xls; - chart:
export.xlsthroughmodel_export-chart-xls.
Exports defined in the schema are added by id and can replace a default export.
| Property | Type | Description |
|---|---|---|
id |
string |
Identifier. |
label |
string |
Label translated through view.{view_id}.exports. |
icon |
string |
Optional icon. |
controller |
string |
Export controller. |
view |
string |
View passed to the controller; default: current view. |
The export menu opens a backend URL with get, entity, view_id, domain, lang, controller, nolimit: true, and params.
List Layout¶
A list expects a flat layout:
{
"layout": {
"items": [
{"type": "field", "value": "name", "width": 40, "sortable": true},
{"type": "field", "value": "status", "width": 20}
],
"interactions": {
"click": true
}
}
}
List Items¶
| Property | Type | Description |
|---|---|---|
value |
string |
Model field. Items without a valid field are ignored. |
label |
string |
Fallback label, overridden by the model and then translations. |
width |
number |
Width as a percentage. Minimum forced to 10, then the total is normalized to 100. |
visible |
boolean or Domain |
If false, the column is ignored at layout time; if a domain, visibility is evaluated per row. |
sortable |
boolean |
Enables sorting by clicking the header. |
align |
left, center, right |
Alignment. Default: right for integer, float, time except id, otherwise left. |
widget |
object |
Widget configuration override. |
styles |
array |
Conditional styles evaluated per row for scalar fields. |
Interactions:
| Property | Effect |
|---|---|
layout.interactions.click |
Configure how clicking a row reacts (opening object in a ne context or not). |
layout.interactions.autofocus |
Configure how selectable fields react. |
By default, clicking a row opens form.{name} on the same object. If the list is a widget, closing that context triggers a refresh of the parent view.
Form Layout¶
A form expects a nested structure:
{
"layout": {
"groups": [
{
"id": "main",
"label": "Main",
"sections": [
{
"id": "general",
"label": "General",
"rows": [
{
"columns": [
{
"width": 50,
"items": [
{"type": "field", "value": "name", "width": 100},
{"type": "label", "id": "hint", "value": "Static text"}
]
}
]
}
]
}
]
}
],
"interactions": {
"autofocus": true
}
}
}
Supported Nodes¶
| Level | Main Properties |
|---|---|
groups[] |
id, label, visible, sections[] |
sections[] |
id, label, visible, rows[] |
rows[] |
visible, columns[] |
columns[] |
width, align, visible, items[] |
items[] |
type, value, id, label, width, align, visible, readonly, required, widget |
Widths are converted to Material Design columns on a 12-column grid:
span = round(width / 100 * 12)
Static labels (type: "label") are rendered with the label widget and translated through view.{view_id}.layout.{id}.
Autofocus¶
By default, LayoutForm focuses the first direct input of the first widget after rendering. To disable it:
{
"layout": {
"interactions": {
"autofocus": false
}
}
}
Onchange¶
When a widget emits _updatedWidget, the form:
- updates the local object;
- calls
model_onchangewhen the value is small enough or when it is a file with metadata; - applies returned value changes;
- updates returned domains, selections, or field definitions;
- triggers
onchangeViewModel().
required fields are checked by the layout before saving. For a many2one, presence is checked through object_id.
Search Layout¶
LayoutSearch reuses the form structure: groups -> sections -> rows -> columns -> items. It is used for advanced search.
Main differences:
- relational widgets do not show create/open actions;
advanced_search.submit: "auto"applies changes immediately;advanced_search.submit: "manual"accumulates changes and shows a search button;- changes feed the parent view parameters, then trigger its refresh.
Dashboard Layout¶
The dashboard also uses groups -> sections -> rows -> columns -> items, but each expected item embeds another view.
| Property | Type | Description |
|---|---|---|
entity |
string |
Entity of the embedded view. |
view |
string |
View in type.name format; default list.default. |
domain |
Domain |
Domain of the embedded view. |
width |
number |
Cell width on the 12-column grid. |
height |
string |
Cell CSS height. |
Rows can also define height as a percentage of the context's available height.
Chart Layout¶
LayoutChart reads its configuration from layout.
| Property | Default | Description |
|---|---|---|
type |
bar |
Chart.js type (bar, line, pie, etc.). |
stacked |
false |
Dataset stacking for some chart types. |
group_by |
range |
Grouping sent to the chart controller. |
field |
created |
Reference temporal field. |
range_field |
absent | Optional range field. |
range_interval |
month |
Grouping interval. |
range_from |
date.this.year.first |
Relative start date. |
range_to |
date.this.year.last |
Relative end date. |
entity |
optional | Target entity sent to the controller. |
datasets |
required | Chart datasets. Each dataset defaults to label: "label" and operation: ["COUNT", "object.id"]. |
The called controller is core_model_chart, unless overridden by view_schema.controller.
In grid mode, the result is rendered as a table. Otherwise, it is rendered in a Chart.js canvas.
Widgets and Field Configuration¶
WidgetFactory.getWidgetConfig() merges the model schema and the view item.
| Source | Effect |
|---|---|
model label, description, help, usage, type, readonly, required, selection, foreign_object, foreign_field, domain, visible |
Defines the widget base. |
view item label, description, help, readonly, required, align, sortable, visible, widget |
Overrides or completes the base. |
item.widget |
Final widget configuration override. |
Important rules:
- if the model defines
selection, the widget becomesselect; usagecan convert the final type (text/html->text,uri/url->link,image/png->file, etc.);purpose: "create"forcesreadonly: false, even if the model is readonly;visibleis serialized as JSON before evaluation;layoutislistonly for views of typelist; otherwise it isform.
Main widget types:
| Final Type | Widget |
|---|---|
boolean |
WidgetBoolean |
date |
WidgetDate |
time |
WidgetTime |
datetime |
WidgetDateTime |
integer |
WidgetInteger |
float |
WidgetFloat |
string |
WidgetString, except color usages rendered with WidgetSelect |
text |
WidgetText |
link |
WidgetLink |
binary / file |
WidgetFile, WidgetImage, or WidgetSignature depending on usage |
pdf |
WidgetPdf |
upload |
WidgetUpload |
many2one |
WidgetMany2One |
one2many |
WidgetOne2Many |
many2many |
WidgetMany2Many |
label |
WidgetLabel |
Relational Fields¶
many2one¶
A many2one is displayed from {id, name}. The model always loads field.name.
Notable options:
| Option | Behavior |
|---|---|
widget.view |
Default view used for relational sub-contexts. |
widget.header.view |
View used to open the related object. |
widget.header.actions.ACTION.CREATE |
Can contain domain and view_id for creation. |
widget.header.actions.ACTION.OPEN |
Enables/disables the open button. |
widget.header.actions.ACTION.SELECT |
Enables/disables advanced selection. |
widget.header: false or widget.header.actions: false |
Disables create/open/select. |
component |
Delegates creation to an application component. |
In edit mode, the widget allows:
- resetting the relation;
- opening the related object;
- creating through a form or component;
- instant creation through
ApiService.create(foreign_object, {name}); - advanced search through a
purpose: selectlist.
one2many and many2many¶
These fields are rendered as embedded views. The main collection does not load relations.
The sub-view domain is built from:
- the model
domain; - the widget
domain; - join condition on
foreign_fieldandobject_id; ids_to_addandids_to_delto reflect local changes.
For many2many, selection is available only in edit mode. Local removals are represented by negative ids.
Conditional Visibility¶
visible properties can be:
trueorfalse;- the string
"true"or"false"; - a JSON string representing a domain;
- a domain array.
Evaluation uses:
object, user, parent, env
Visibility domains are used for:
- actions;
- routes;
- groups, sections, rows, columns, and items;
- widgets;
- action-dialog parameters.
Grouping¶
group_by is used by LayoutList.
Accepted forms:
{
"group_by": ["date"]
}
{
"group_by": [
{
"field": "time_slot_id",
"operation": ["SUM", "object.qty"],
"operations": {
"qty": {"operation": "SUM", "usage": "number/integer"}
},
"order": "name",
"usage": "date/month",
"open": true,
"colspan": 2
}
]
}
| Property | Description |
|---|---|
field |
Grouping field. |
operation |
Aggregate displayed in the group label. |
operations |
Per-column aggregates, displayed in the remaining group cells. |
order |
For object keys, subfield used as sort key. |
usage |
Usage applied to the group label, especially for dates. |
open |
When defined on the first grouping, forces the open/closed state after refresh. |
colspan |
Number of columns covered by the group label cell. |
Dates are normalized as YYYY-MM-DD keys and formatted through getMomentFormatFromUsage(). Empty keys become no_value with label empty.
Operations¶
Operations are used:
- in
group_by[].operation; - in
group_by[].operations; - at root-level
operationsto display aggregate rows under the table.
Supported operators:
| Operator | Description |
|---|---|
SUM |
Sum of non-null values. |
COUNT |
Count of non-null values, or object count if no field is provided. |
MIN |
Minimum. |
MAX |
Maximum. |
AVG |
Incremental average. |
DIFF |
Difference between two nested operations. |
An operation can be a string or an array:
{"operation": "COUNT"}
{"operation": ["SUM", "object.amount"]}
{"operation": ["DIFF", ["SUM", "object.total"], ["SUM", "object.paid"]]}
Root-level example:
{
"operations": {
"total": {
"amount": {
"operation": ["SUM", "object.amount"],
"usage": "amount/money:2",
"prefix": "EUR "
}
}
}
}
The final value is formatted by Widget.toString() according to the model type or provided usage.
Routes¶
Routes are handled by LayoutList as inline buttons when inline: true.
| Property | Type | Description |
|---|---|---|
inline |
boolean |
Only inline routes are rendered in rows. |
visible |
boolean or Domain |
Evaluated per row. |
context |
object |
Context to open. |
context.domain |
Domain |
object.field references are replaced by values from the current object. |
context.display_mode |
popup |
Opens the context in a popup through the frame listener. |
Rich Text¶
WidgetText uses Quill in form edit mode.
Rules by usage:
| Usage | Edit Mode | View Mode |
|---|---|---|
text/html |
HTML normalized for Quill, paragraphs ensured. | HTML rendering. |
text/plain |
Quill editing, but output is plain text through getText().trim(). |
Line breaks converted to <br />. |
text/json |
Text editing. | Escaped rendering in <pre><code class="language-json">. |
The widget debounces Quill changes for about 1 second and emits _updatedWidget without immediate refresh (refresh = false).
Examples¶
List with Inline Action, Grouping, and Aggregates¶
{
"controller": "model_collect",
"order": "date",
"sort": "desc",
"layout": {
"items": [
{"type": "field", "value": "date", "width": 20, "sortable": true},
{"type": "field", "value": "customer_id", "width": 30},
{"type": "field", "value": "amount", "width": 20, "sortable": true}
]
},
"group_by": [
{
"field": "date",
"usage": "date/month",
"open": true,
"operations": {
"amount": {
"operation": ["SUM", "object.amount"],
"usage": "amount/money:2"
}
}
}
],
"actions": [
{
"id": "action.validate",
"label": "Validate",
"icon": "done",
"inline": true,
"controller": "sale_order_validate",
"visible": ["status", "=", "draft"],
"confirm": true
}
]
}
Form with Sections and Custom Save¶
{
"mode": "view",
"header": {
"actions": {
"ACTION.SAVE": [
{"id": "SAVE_AND_CLOSE"},
{"id": "SAVE_AND_CONTINUE", "controller": "sale_order_update-custom"}
]
}
},
"layout": {
"groups": [
{
"id": "main",
"sections": [
{
"id": "general",
"label": "General",
"rows": [
{
"columns": [
{
"width": 50,
"items": [
{"type": "field", "value": "name"},
{"type": "field", "value": "customer_id"}
]
},
{
"width": 50,
"items": [
{"type": "field", "value": "description", "widget": {"height": 240}}
]
}
]
}
]
}
]
}
]
}
}
many2one Field with Controlled Creation¶
{
"type": "field",
"value": "customer_id",
"widget": {
"view": "list.default",
"header": {
"view": "form.default",
"actions": {
"ACTION.CREATE": [
{
"view_id": "form.create",
"domain": ["category", "=", "customer"]
}
],
"ACTION.OPEN": true,
"ACTION.SELECT": true
}
}
}
}