Widget & View Registries
The React back-office app has two registries that module authors extend: the field
widget registry (how a single field is rendered/edited) and the view-type registry
(what each view is and what it can do). Both live under app/src/ and are pure React
— they have nothing to do with the Preact islands framework.
Field widget registry
Section titled “Field widget registry”A widget renders one field across the app’s render modes. The registry lives in
app/src/components/ui/fields/registry.jsx and exposes:
createWidget(config)— build a widget definition.registerWidget(name, widget)— add it to the registry undername.getWidget(name)— look one up (returnsnullif absent).hasWidget(name),getWidgetNames()— introspection.renderWidget(name, props)— resolve and render (used internally by the field renderer).
A backend view arch references a widget by name (widget: ColorInput); the field renderer
calls renderWidget('ColorInput', { mode, value, onChange, ... }), which resolves the
registered definition and renders it.
createWidget
Section titled “createWidget”createWidget({ name, // string — registry key supportedModes = ['form', 'list', 'formlist', 'kanban', 'display'], render, // (props) => ReactNode defaultProps = {}, // merged under incoming props})render receives the full prop bag merged over defaultProps. The key prop is mode,
one of:
| Mode | Where it renders |
|---|---|
form | A field on a Form view (editable input) |
list | A cell in a List view (typically read-only inline) |
formlist | A cell inside an embedded list (One2many / Many2many table) on a form |
kanban | A field on a Kanban card |
display | Read-only display anywhere |
These five strings are the default supportedModes. Declare a narrower set if your
widget cannot render in a given mode.
Mode resolution and fallback
Section titled “Mode resolution and fallback”renderWidget enforces supportedModes. If the requested mode is not supported but the
widget supports display, it transparently re-renders in display mode; otherwise it logs
a warning and renders an “Unsupported widget” placeholder. A widget not found in the
registry logs a warning and renders nothing.
Worked example
Section titled “Worked example”Widgets self-register at import time: define with createWidget, then registerWidget.
import React from 'react';import { Badge, TextInput } from '@mantine/core';import { createWidget, registerWidget } from '../registry';
const MyBadgeWidget = createWidget({ name: 'MyBadge', supportedModes: ['form', 'list', 'formlist', 'kanban', 'display'], defaultProps: { color: 'gray' }, render: ({ mode, value, onChange, color, label, placeholder }) => { // Read-only modes: render a badge if (mode !== 'form') { return value ? <Badge color={color}>{value}</Badge> : null; } // Form mode: render an editable input return ( <TextInput label={label} placeholder={placeholder} value={value ?? ''} onChange={(e) => onChange(e.currentTarget.value)} /> ); },});
registerWidget('MyBadge', MyBadgeWidget);Register the file for side-effect import in
app/src/components/ui/fields/index.js (each widget module is imported there to trigger
its registerWidget call), alongside the existing import './inputs/ColorInputWidget';
lines. Once imported, a view arch can use widget: MyBadge.
Conventions still apply. Use theme-aware Mantine colors (never hardcoded hex), and when your widget shows user-facing text, pass it through
t()with the English string as the key. See React Conventions.
Gated widgets
Section titled “Gated widgets”Some widgets require a configuration/license activation. The registry keeps a
_gatedWidgets set (currently Gantt and FinancialReport); when a gated widget is
requested without props._cacheReady, renderWidget returns a LockedWidget placeholder
instead. Use isGatedWidget(name) to check.
View-type registry
Section titled “View-type registry”While widgets render fields, the view-type registry describes whole view types. It
lives in app/src/views/registry.js and is the single source of truth for what each view
type can do.
The motivation is captured in the file itself: capabilities used to be encoded as
scattered identity checks (viewType !== 'Calendar'), which silently break when a new
type shares the same trait. Instead, each view type is declared once, as data, and call
sites branch on a capability rather than a string literal.
The capability record
Section titled “The capability record”VIEW_TYPES maps each view-type name to a flat record of boolean capabilities:
| Capability | Meaning |
|---|---|
searchable | Browses records — gets the search bar / Browse (Filter) panel |
grouping | Records can be grouped into buckets |
filterPanel | Shows the left-hand Browse/Filter panel on desktop |
builderEditable | Has an arch editor in the Builder (“Edit this view”) |
builderManaged | Can be created/managed in the Builder’s view manager (superset of builderEditable) |
fullHeight | Needs a height:100% container rather than shell-scrolled layout |
paginated | Shows the footer/pagination bar |
recordToolbar | Shows the global record toolbar (New / Import) |
Declared types include Kanban, List, Form, Search, Import, Chart, Pivot,
Calendar, Gantt, TimelineHeatmap, FinancialReport, CustomerStatements,
BankReconciliation, EcoDiff, PlanningGrid, PosRegister, Shopfloor, OrgChart,
Collaborate, Dashboard and ActivityStream. The insertion order mirrors the
ViewRenderer dispatch order.
The API
Section titled “The API”import { VIEW_TYPES, VIEW_TYPE_NAMES, supports, viewTypesWith } from '@/views/registry';
supports('List', 'grouping'); // → truesupports('Calendar', 'grouping'); // → false (events lie on a date axis)supports('Whatever', 'grouping'); // → false (unknown types are safe-false)
viewTypesWith('searchable'); // → ['Kanban', 'List', 'Chart', 'Pivot', ...]supports(type, capability)— branch on a capability instead of comparing to a string. Unknown types reportfalse.viewTypesWith(capability)— enumerate every type that has a capability, instead of hand-maintaining a list.
Adding a view type
Section titled “Adding a view type”Add one entry to VIEW_TYPES (built with the cap({...}) helper, which fills the
rest of the capabilities as false) and add the component to the ViewRenderer dispatch.
Nothing else needs to enumerate view types by hand — every call site that cares branches on
a capability via supports / viewTypesWith.