Skip to content

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.

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 under name.
  • getWidget(name) — look one up (returns null if 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({
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:

ModeWhere it renders
formA field on a Form view (editable input)
listA cell in a List view (typically read-only inline)
formlistA cell inside an embedded list (One2many / Many2many table) on a form
kanbanA field on a Kanban card
displayRead-only display anywhere

These five strings are the default supportedModes. Declare a narrower set if your widget cannot render in a given mode.

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.

Widgets self-register at import time: define with createWidget, then registerWidget.

app/src/components/ui/fields/inputs/MyBadgeWidget.jsx
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.

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.

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.

VIEW_TYPES maps each view-type name to a flat record of boolean capabilities:

CapabilityMeaning
searchableBrowses records — gets the search bar / Browse (Filter) panel
groupingRecords can be grouped into buckets
filterPanelShows the left-hand Browse/Filter panel on desktop
builderEditableHas an arch editor in the Builder (“Edit this view”)
builderManagedCan be created/managed in the Builder’s view manager (superset of builderEditable)
fullHeightNeeds a height:100% container rather than shell-scrolled layout
paginatedShows the footer/pagination bar
recordToolbarShows 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.

import { VIEW_TYPES, VIEW_TYPE_NAMES, supports, viewTypesWith } from '@/views/registry';
supports('List', 'grouping'); // → true
supports('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 report false.
  • viewTypesWith(capability) — enumerate every type that has a capability, instead of hand-maintaining a list.

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.