🧩 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.
- Developer → Metadata → Widgets. The first column is the human name of
the type, not the technical
SubtypeStepper. - 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.
- Open the document type (or the dictionary) → the Widgets tab.
- 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.
- 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.
- In the catalog, press Create.
- The type first (the groups: On a record card / Documents only / Desktop).
- The caption in the catalog is optional (empty = the type's name).
- 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
actionsis not an empty widget.deriveQuickActionsreads the user's own menu (already permission-filtered byGET /api/menu/my) and offers up toDERIVED_LIMITtiles: theDocumentleaves indisplayOrder, deduped by target, plus the firstCustomReport/VirtualTotal/Reportleaf 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.
resolveQuickActionsreturns only tiles it could resolve against the metadata the user can see. initialSubtypeon a record window is a subtype VALUE, not a metaId. The config stores the metaId (stable across renames), sosubtypeValueOftranslates it throughapi.getSubtypesbefore 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 storedconfigJson, 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 theunknownlist 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