Skip to main content

Attributes on the Frontend

The backend's Attribute Engine lets an administrator add fields to almost any entity at runtime. On the frontend those fields have to appear in forms, validate, type-check, and show up as table columns — without a code change per field. Three small modules in src/components/attribute-fields/ do that.

Fetching definitions

src/core/hooks/useAttributeDefinitionService.ts:

HookUse
useAttributeDefinitions()Every definition — filter client-side by entityType / isActive
useAttributeDefinitionsForEndpoint(method, path)Just the definitions that apply to one API operation, e.g. ('POST', '/stocks/NewArrivalBatch'). Prefer this on a specific form
useActionsForEntityType(entityType)The list of actions an entity supports — for the definition editor's "Action" dropdown

getActiveAttributeDefinitions(defs, entityType) (in attribute-table-columns.ts) is the standard filter+sort: active only, ordered by displayOrder.

Rendering a field

<AttributeField definition={def} control={form.control} /> renders one field, switching on definition.dataType:

dataTypeControl
text<Input>
number<Input type="number">, empty → undefined
boolean<Switch> in a bordered row
date<Popover> + <Calendar>
select<Select> over definition.options

The field name is definition.key, so the value lands in the form under that key.

Validating

buildAttributeSchema(definitions) builds a Zod object — z.string() / z.number() / z.boolean() / z.date() / z.enum(options) per dataType, wrapped in .optional() unless definition.required.

The pattern on a form with custom fields:

const combinedSchema = BaseFormSchema.merge(
buildAttributeSchema(activeProductDefinitions)
);
// ...
<Form ... resolver={zodResolver(combinedSchema)}>
{/* base fields */}
{activeProductDefinitions.map((d) => (
<AttributeField key={d.id} definition={d} control={form.control} />
))}

On submit, collect the dynamic values back out by key:

const attributeValues = Object.fromEntries(
activeProductDefinitions.map((d) => [d.key, data[d.key]])
);

and send them as the payload's attributeValues object.

Showing values in tables

formatAttributeValue(definition, value) renders a stored value consistently regardless of table implementation — for empty, Yes/No for boolean, YYYY-MM-DD for date, String(value) otherwise. The data tables build one extra column per active definition:

...getActiveAttributeDefinitions(defs, "Product").map((d) => ({
accessorFn: (row) => row.attributeValues?.[d.key],
header: d.label,
}))