ZuloOne
English
Sign in
Menu

Grid columns: what the user reads is not what the row stores

Updated

Every list, journal and document table part in the app is one control — StandardGrid (frontend/src/kit/StandardGrid.tsx). A column of that grid has two projections of the same cell, and getting the second one wrong is how a GUID reaches the user.

← Developer wiki

The two projections#

{
  title: 'Item',
  dataIndex: 'Item',
  // What the cell SHOWS. Free to be a React node.
  render: (v, row) => <ReferenceValue dictionaryId={dictId} value={row.Item} />,
  // What the cell IS, as text. Must be a plain string.
  exportText: (row) => refText(dictId, row.Item),
}

render is the visible cell; exportText is the same cell as text. They are separate because a React node cannot be stringified: the row stores 4834f2c2-33b4-4507-9232-8920965b8fb8, the cell shows «Baguette 300g», and nothing in between can derive one from the other.

Everything textual reads exportText#

Path Reads
The cell on screen render
Quick search exportText
Column filter — the Values check-list exportText
Column filter — the Text filters tab exportText
Sorting the raw value — except a reference (GUID), which sorts by exportText
Ctrl/Cmd+C on the active cell exportText
Print list and the CSV / Excel / JSON export exportText

The Values check-list popover sizes to the longest label (clampColumnFilterWidth in frontend/src/kit/columnFilterPanel.ts, 260–520 px) and scrolls vertically once the list is taller than min(24rem, 60vh). A fixed 260×190 box used to clip 1C metadata names and hide rows without a scrollbar.

The helpers are in frontend/src/kit/columns.ts: textProjectionsOf(columns) builds the accessor → projection map once, cellTextOf reads a cell as text, sortKeyOf picks the ordering key. Unit tests: kit/columnText.test.ts, kit/columnFilterPanel.test.ts.

Warning

A reference column without exportText still looks right on screen — and then its filter offers a list of GUIDs, its quick search never matches the name the user typed, and it sorts by a random-looking hex string. The column is not broken where you see it; it is broken everywhere else.

Getting the text for a reference#

useReferenceTexts (components/ReferenceSelect.tsx) resolves a set of dictionaries once and hands back a lookup:

const refText = useReferenceTexts(
  fields.map((f) => f.referenceDictionaryId).filter((id): id is string => Boolean(id)),
);
// …
exportText: (r) => refText(dictionaryId, r[f.name]),

It returns '' for Guid.Empty, the resolved displayValue once loaded, and the id itself while the dictionary is still loading or when the record is gone. Its identity is stable while the loaded items are: callers hang it on memoized column sets, so a fresh closure on every render would rebuild every column, and a frozen one would pin the grid to the pre-load snapshot where every reference still reads as a GUID.

For a document pointer — or any other GUID that names a record or type — use useDocumentLabelText the same way. labeledText in lib/fields.ts is the shared print/CSV projection: dictionary reference first, then resolve-labels for a stored record id (including a String/Guid field that just happens to hold one), otherwise formatValue.

Related columns (useRelatedColumns) walk the same way: the leaf is formatted for the screen and for exportText. A Guid leaf without a Reference EDT still resolves.

A document journal is the same grid. Number, date and subtype stay first; every header field follows, with the same render / exportText pair as a dictionary list. Hide extras in View → Pick columns. Related columns of header references (Warehouse.Name) stay in that picker; they are not the header itself.

System columns DocumentMetaId and OwnerMetaId resolve too — they point at another record. MetaId / MovementMetaId / AnalyticSetMetaId stay the raw id: that is the row's own identity, not a name the user reads.

Sorting is deliberately not projected#

A date column's text is 03.10.2026 and a quantity's is 10; sorting those as strings puts October before February and 10 before 2. So the sort key stays the RAW value, and only a reference — whose raw value is a GUID with no meaningful order — falls back to the projection (sortKeyOf).

Lists do not carry photos#

GET /api/data/{table} does not select Image/Binary bytes (QueryAsync(..., includeBinary: false)). A 40-row Item page with Item.Image used to wait on megabytes of base64 — the spinner showed «0 of N» until the body arrived. The column stays on the row as 1 or null (the blob exists or not, no TOAST). Cards and the Photo column then load GET /api/data/{table}/{id}/image/{field}?w=160, which returns a shrunk JPEG/PNG; the <img> sends the office cookie. The record form still loads the original via GET /api/data/{table}/{id}.

Where next#

Was this page helpful?