ZuloOne
English
Sign in
Menu

🧩 Widgets: the catalog and the tiles on a card

Updated

The Widgets catalog holds tile presets, not the tiles on a form themselves. One preset (Progress, say) is placed on many documents. A new catalog row is created only for a second variant of the same type.

← Developer wiki · The forms system

The ⓘ button on the catalog and on a type's Widgets tab opens this same guide inside the application. The body is Markdown (tables, images, lists).

Three different meanings of the word "widget":

What Where it is configured What for
A field widget (scan, textarea) the form layout designer how a single field is drawn
A catalog preset (MetaWidget) Developer → Metadata → Widgets a tile preset (type + data + appearance)
A placement on an object (MetaObjectWidgetPlacement) the Widgets tab of a document / dictionary type a tile in a journal or card slot

The sections below cover the catalog and placements. Field-level widgets live in the form designer.

Guide 1. Put a ready-made tile on a card#

The Core seed presets are already in the catalog (the Ready-made badge): Attachments, Notes, Progress, Order Summary and others.

  1. Developer → Metadata → Widgets. The first column is the human name of the type, not the technical SubtypeStepper.
  2. Open a ready-made preset. In the Form palette block tick All documents (or specific types). For attachments/notes you can also tick All dictionaries.
  3. Open the document type (or the dictionary) → the Widgets tab.
  4. Pick a slot. List is the strip above the journal (Chart and KPI). Header, Side, Body, Footer are the record card. Tab / hover is the workspace tab card.
  5. Click a preset in the right-hand list — it lands in the slot.

Order within a slot: the Move up / Move down arrows, or dragging by the handle. To another slot: the layout button, or drag the tile onto the slot's button.

The gear on a placement holds the settings of this tile on the object (the chrome, the fields), not of the whole preset.

Titles in every language. The chrome title and the KPI label are typed in the interface language: with the interface in Russian, the text you type is the Russian one, and the English text stays as it was. In the placement file both are a string or a language object, like form labels:

"configJson": "{\"label\":{\"en\":\"Items\",\"ru\":\"Товары\"},\"table\":\"Item\",\"mode\":\"count\"}"

A language with no text shows the English one.

Guide 2. When to create a new preset#

The list already holds one ready-made preset per form type. You only need another one if you want a second variant: different fields, a different title, a different set of commands.

  1. In the catalog, press Create.
  2. The type first (the groups: On a record card / Documents only / Desktop).
  3. The caption in the catalog is optional (empty = the type's name).
  4. The technical name is filled in for you (Attachments2). Users never see it.

Do not create "Progress for the Order" and "Progress for the Invoice". One preset plus the All documents tick.

Guide 3. The preset builder#

Double-click a catalog row.

Block What for
1. Tile type What gets drawn. The ready-made ones already have it set.
2. Data The fields and the tables. The sample record fills in real names.
3. Appearance The tile's title and its caption in the catalog.
4. Where it goes Form / desktop / both. The palette is here too: all documents, all dictionaries or a list of types.
5. Preview What the tile looks like on the sample record.

The documents-only types (Progress, Order Summary, Related Documents, Customer Snapshot) cannot be attached to a dictionary: there is no All dictionaries tick, and the server rejects such assignments.

FieldGroup, CommandButtons and RecordImage are in the catalog but not in the palette by default — tick the documents/dictionaries first, otherwise they will not be there in the right-hand list on the object's tab.

The slots#

Slot Where
List above the journal / dictionary grid (list-capable tiles only: Chart, KPI)
Header above the fields on the record
Side a column at the side of the record
Body in the flow of the card
Footer below the fields
Tab / hover the card shown when hovering a workspace tab

If the tab slot is empty, the hover card is silent. The subtype on the left of a document's tab: a placement for All subtypes or for a single subtype.

The ready-made types (form)#

Name in the list Type Where What the seed assigns
Attachments attachments card all documents and dictionaries
Internal Activity activity card all documents and dictionaries
Notes notes card all documents and dictionaries
Order Summary document-totals card all documents. On a live card it reads the lines out of FormRuntime.tableParts and does not wait for a save
Timesheet grid timesheet-grid card documents only. A live Days grid (a click writes into tableParts)
Progress subtype-stepper card all documents
KPI record-kpi card / list all documents and dictionaries
Customer Snapshot related-snapshot card all documents
Chart chart card / list all documents and dictionaries
Field Group field-group card none — assign it yourself
Command Buttons command-buttons card none — assign it yourself
Related Documents related-documents card all documents
Record Image record-image card none — assign it yourself

The desktop-only types (count, total, script, …) are not created by the seeded — a dashboard preset is created by its author. The field and editor contract is in the forms system.

Progress after a return#

Progress shows the document's current pass through its subtypes. A document can go back: an order rejected from Submitted returns to Draft. After that:

  • the current step is dated by its latest landing and says «returned from «Submitted»»;
  • each earlier step is dated by its latest landing before that;
  • a later step the document had already reached is pending again, with «earlier: 30 Sep 2026, 19:02» under it.

Example, order № 2757: Draft 21 Sep → Submitted 30 Sep 13:50 → Draft 15:05 → Submitted 19:02 → Draft 1 Oct 12:43. The widget shows Draft ✓ 1 Oct 12:43 (returned from «Submitted») and Submitted ○ (earlier: 30 Sep 19:02). Tick every visited step and it would show both ticked, Draft 1 Oct above Submitted 30 Sep, as if the order had been submitted before its draft existed. The logic is buildProgressSteps in frontend/src/widgets/record/subtypeProgress.ts.

The desktop types#

The desktop is not the catalog. A tile on the dashboard is an entry in the dashboard.widgets user setting (api.getUserSetting / saveUserSetting), not a MetaWidget row: { id, type, layout: { x, y, w, h }, config }. It is per user, there is no way to publish one arrangement to a role, and a tile is drawn by the type's renderer from pages/dashboard/registry.tsx — the same registry the card tiles come from, filtered by scopes: ['desktop'].

A dashboard that has never been touched is seeded with quick-actions, chart, total, recent-changes (WidgetGrid.DEFAULT_WIDGETS). The seed runs only while the setting is absent: an empty array is a valid saved state, which is why the edit bar carries Restore defaults — without it a user who removed every tile had no way back.

quick-actions#

The row of "start a sales order / build a report / open the till" tiles. Config (widgets/quickActions.ts):

type QuickAction =
  | { kind: 'menu';            menuMetaId: string;   label?: string }
  | { kind: 'new-document';    documentTypeMetaId: string; subtypeMetaId?: string; label?: string }
  | { kind: 'new-dict-record'; dictionaryMetaId: string;   label?: string }
  | { kind: 'page';            path: string;  label?: string }   // in-app, docked as a tab
  | { kind: 'url';             url: string;   label?: string };  // anything outside
interface QuickActionsConfig { actions: QuickAction[]; columns?: number }

Three rules worth knowing before you extend it:

  • An empty actions is not an empty widget. deriveQuickActions reads the user's own menu (already permission-filtered by GET /api/menu/my) and offers up to DERIVED_LIMIT tiles: the Document leaves in displayOrder, deduped by target, plus the first CustomReport / VirtualTotal / Report leaf last. The derived tiles vanish the moment the user adds one of their own.
  • A tile whose target is gone is dropped, not drawn dead. resolveQuickActions returns only tiles it could resolve against the metadata the user can see.
  • initialSubtype on a record window is a subtype VALUE, not a metaId. The config stores the metaId (stable across renames), so subtypeValueOf translates it through api.getSubtypes before the card is opened. A metaId passed straight through selects nothing, silently.

A menu action is dispatched through layout/menuTargets.ts + layout/useMenuLauncher.ts — the single rule the ribbon, quick search and the quick-action tiles all share. Put new target types there, not in a caller.

The resolution is pure and lives away from React (quickActions.ts), so a change to it is testable in quickActions.test.ts without rendering anything.

total#

One number (or one small chart) over a register, a virtual total or a custom report. The source is picked by kind + metaId, and the columns are the same Variable[] the totals report tab edits — a widget is a report over one source, drawn small.

Because the columns are the same thing, the widget reuses the saved report views as column sets: with a source chosen, VariablesPanel is handed its optional sets prop and shows the report.pickSet Select plus the report.allSets tick, exactly as the report tab does. Own sets come from ReportSource.loadViews(); the tick widens the list to the whole registry (api.getReportViewRegistry).

Two decisions that are easy to undo by accident:

  • Picking a set copies its columns and nothing else. A saved view also carries a period, groups, filters and filter modes. The widget owns those through its own fields, so applying them from a set would silently overwrite what the user had just configured one control above. It is a copy, not a link: the widget keeps what it was given when the set is edited later. That is also why the picked set's metaId is not stored in the widget config — a persisted id would read as a binding the widget does not honour.
  • Reading a set is one rule, shared. registers/reportVariableSets.ts (readVariableSet) parses the stored configJson, migrates the legacy v2 shape (kinds / exprColumns / columnStyles) into variables, stamps an id on every column that lacks one, and returns the variables the source does not offer separately. Both the report tab and the widget call it; neither parses a view itself. A set saved for another object may well name a resource this one lacks, and those columns would otherwise read as a silent zero on the tile — hence the unknown list and the orange notice under the picker. Tests: reportVariableSets.test.ts.

Changing the source clears the columns, and therefore clears the pick that produced them (changeSource in TotalWidgetConfig.tsx) — a stale set name sitting over a column list it no longer describes is worse than an empty box.

Names: what a human sees#

The seed preset files are called SubtypeStepper, DocumentTotals — that is how the files are named in the workspace. In the catalog grid the first column is the name of the type ("Progress"), and the technical name is the last column.

When the Caption is the same as the Name, the UI shows the type's caption, not the PascalCase.

Rows like AsgWid2a5dfed3 and WsWid… are leftovers from API tests. They have no business being in Core/: the tests stamp TestBench, and POST /api/dev/test-artifacts/sweep sweeps up widgets, assignments and placements (including the files whose suffix only shows up inside the JSON). In the catalog, the leftovers are hidden behind the Show test leftovers tick.

The workspace#

File What
Widgets/<Name>.widget.json the preset + surface (dashboard | form | both; empty = dashboard)
WidgetAssignments/<Widget>.<ObjectType>[.<Object>].json who sees the preset in the palette
WidgetPlacements/<Object>.<Slot>.<n>.json the tile on the card + configJson; import-all places it along with the tree

Core/Widgets is edited by the platform alone (the seeder). Other people's models — additively. The file contract is docs/WORKSPACE.md.

The API (in brief)#

  • The catalog: /api/metadata/widgets
  • The assignments: /api/metadata/widget-assignments
  • An object's palette: GET /api/metadata/widgets/assigned?objectType=&objectMetaId=
  • The placements: /api/metadata/object-widget-placements
  • Deleting a preset that an assignment or placement still points to returns 409

Same topic for other readers: User

Where next#

Was this page helpful?