@asteby/metacore-runtime-react 13.10.2 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 15.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab41d75: Make declarative dynamic-table option badges and relation chips feel alive.
8
+
9
+ Options/select/status badges that ship without an explicit `color` from the
10
+ backend now get a deterministic, cohesive color derived from the option value
11
+ (fallback label) instead of rendering as dead gray text. Same value always maps
12
+ to the same hue, and equal words share a color.
13
+ - `@asteby/metacore-ui/lib` adds `optionColor(key)` (curated 16-hue Tailwind-500
14
+ palette, FNV-1a hash, light/dark safe), plus `optionColorBadgeStyles`,
15
+ `relationChipStyles`, and the exported `OPTION_PALETTE`.
16
+ - `OptionBadge` uses the explicit `color` when present, otherwise derives one via
17
+ `optionColor`, and renders the option's lucide `icon` before the label.
18
+ - `RelationCell` (resolved FK chips for category/brand/supplier/…) now gets a
19
+ subtle deterministic color keyed on the related label — kept lighter than enum
20
+ badges (soft tint, no heavy fill) so the two remain distinguishable.
21
+
22
+ All colors come from hex-derived inline styles, so they render correctly
23
+ regardless of the host's tailwind safelist.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [ab41d75]
28
+ - @asteby/metacore-ui@2.3.0
29
+
30
+ ## 14.0.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 6299af7: Pro dynamic-table cells + relation/option multi-select filters
35
+
36
+ `DynamicTable` now renders resolved FK relations and option/type columns, and
37
+ filters them server-side — generically, for every declarative addon.
38
+
39
+ **Cells (`dynamic-columns.tsx`)**
40
+ - `relation` renderer: a column carrying a `ref` (belongs_to FK) or
41
+ `cellStyle: 'relation'` renders the backend-resolved sibling
42
+ `row[<key without _id>] = { value, label }` as a clean truncated chip
43
+ (e.g. `category_id` → `row.category.label`). Falls back to the raw id, then
44
+ to an empty marker. Mirrors how `created_by` ships as a `{ name, avatar }`
45
+ sibling for the `creator` renderer.
46
+ - option/type badge: a `select`-style column shipping inline localized
47
+ `options: [{ value, label, color, icon }]` renders the matched option's label
48
+ as a colored `OptionBadge` (e.g. `product_type: "storable"` → the
49
+ "Almacenable" badge), reusing the same badge path as `badge`/`status`.
50
+
51
+ **Filters (`dynamic-table.tsx` + `FilterableColumnHeader`)**
52
+ - New `dynamic_select` filter type: a `filterable` `ref` column loads its
53
+ options from `searchEndpoint = /options/<ref>` (prefetched + cached into
54
+ `filterOptionsMap`) and renders the same multi-value checkbox combobox as
55
+ `select`. The backend's explicit `column.filterType` wins; otherwise it is
56
+ inferred from the column shape.
57
+ - `select` and `dynamic_select` filters support MULTIPLE selected values
58
+ (already Set-based in the header; the gate/active-count/loading states were
59
+ generalized to cover `dynamic_select`).
60
+
61
+ ### Patch Changes
62
+
63
+ - Updated dependencies [6299af7]
64
+ - @asteby/metacore-ui@2.2.0
65
+
3
66
  ## 13.10.2
4
67
 
5
68
  ### Patch Changes
@@ -1,3 +1,4 @@
1
+ import type { ColumnDefinition } from './types';
1
2
  import type { GetDynamicColumns } from './dynamic-columns-shim';
2
3
  /** Host-supplied helpers consumed by avatar/image cell renderers. */
3
4
  export interface DynamicColumnsHelpers {
@@ -27,6 +28,24 @@ export interface DynamicColumnsHelpers {
27
28
  * - row with no `status` field → all actions shown.
28
29
  */
29
30
  export declare const isActionAllowedForRowState: (action: any, row: any) => boolean;
31
+ /**
32
+ * Resolves the relation sibling object a backend serves alongside an FK column.
33
+ * For a column keyed `category_id` the data row also carries
34
+ * `row.category = { value, label }` (the FK key with the trailing `_id`
35
+ * stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
36
+ * sibling consumed by the `creator` renderer. Returns the relation key so the
37
+ * cell can read `row[relationKeyFor(col)]`.
38
+ */
39
+ export declare const relationKeyFor: (col: Pick<ColumnDefinition, "key">) => string;
40
+ /**
41
+ * Reads the resolved relation/option label a backend serves for an FK or
42
+ * option column, falling back to the raw value. Pure so the cell renderers and
43
+ * tests share one resolution path:
44
+ * - relation: prefer the sibling `{ value, label }` object's label.
45
+ * - option: prefer the matched `options[].label` (value compared as string).
46
+ * - else: the raw value coerced to string ('' when nullish).
47
+ */
48
+ export declare const resolveRelationLabel: (col: ColumnDefinition, row: any) => string;
30
49
  /**
31
50
  * Builds the canonical column factory used by `<DynamicTable>` when the host
32
51
  * does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAuJD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAokBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAwHD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAStE,CAAA;AAiED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAgmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -4,8 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // badge (static + endpoint-loaded options), avatar/search, creator/user,
5
5
  // phone, date, boolean, relation-badge-list, media-gallery, image, plus the
6
6
  // declarative pro renderers url/link, email, currency, number, percent/
7
- // progress, status, tags, color, code/truncate-text, and a generic text
8
- // fallback. The renderer resolves `cellStyle ?? type` for each column.
7
+ // progress, status, tags, color, code/truncate-text, relation (resolved FK
8
+ // chip), option/select badges, and a generic text fallback. The renderer
9
+ // resolves `cellStyle ?? type` for each column.
9
10
  //
10
11
  // The implementation was previously duplicated across multiple host apps
11
12
  // (~550 LOC each, drifting). It now lives here so a single fix propagates
@@ -18,7 +19,7 @@ import * as icons from 'lucide-react';
18
19
  import { MoreHorizontal } from 'lucide-react';
19
20
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
20
21
  import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
21
- import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib';
22
+ import { generateBadgeStyles, getInitials, optionColor, relationChipStyles, } from '@asteby/metacore-ui/lib';
22
23
  import { Progress } from './dialogs/_primitives';
23
24
  import { OptionsContext } from './options-context';
24
25
  import { DynamicIcon } from './dynamic-icon';
@@ -168,7 +169,12 @@ const renderRelationBadges = (items, col) => {
168
169
  };
169
170
  const OptionBadge = ({ option }) => {
170
171
  const isDark = useIsDarkTheme();
171
- const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {};
172
+ // Explicit backend color wins; otherwise derive a stable, cohesive color
173
+ // from the option's value (fallback label) so "dead" gray badges come
174
+ // alive. Inline style (hex-derived) so it works regardless of the host's
175
+ // tailwind safelist — addon-arbitrary classes aren't in the host scan.
176
+ const colorSource = option.color || optionColor(option.value || option.label);
177
+ const colorStyles = generateBadgeStyles(colorSource, { isDark });
172
178
  return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" }), _jsx("span", { children: option.label })] }));
173
179
  };
174
180
  const BadgeWithEndpointOptions = ({ endpoint, value }) => {
@@ -179,6 +185,55 @@ const BadgeWithEndpointOptions = ({ endpoint, value }) => {
179
185
  return _jsx(OptionBadge, { option: option, fallback: String(value) });
180
186
  return _jsx(Badge, { variant: "outline", children: String(value) });
181
187
  };
188
+ /**
189
+ * Resolves the relation sibling object a backend serves alongside an FK column.
190
+ * For a column keyed `category_id` the data row also carries
191
+ * `row.category = { value, label }` (the FK key with the trailing `_id`
192
+ * stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
193
+ * sibling consumed by the `creator` renderer. Returns the relation key so the
194
+ * cell can read `row[relationKeyFor(col)]`.
195
+ */
196
+ export const relationKeyFor = (col) => {
197
+ const k = col.key;
198
+ return k.endsWith('_id') ? k.slice(0, -3) : k;
199
+ };
200
+ /**
201
+ * Reads the resolved relation/option label a backend serves for an FK or
202
+ * option column, falling back to the raw value. Pure so the cell renderers and
203
+ * tests share one resolution path:
204
+ * - relation: prefer the sibling `{ value, label }` object's label.
205
+ * - option: prefer the matched `options[].label` (value compared as string).
206
+ * - else: the raw value coerced to string ('' when nullish).
207
+ */
208
+ export const resolveRelationLabel = (col, row) => {
209
+ const sibling = getNestedValue(row, relationKeyFor(col));
210
+ const label = sibling && typeof sibling === 'object'
211
+ ? sibling.label ?? sibling.name
212
+ : undefined;
213
+ if (label !== undefined && label !== null && label !== '')
214
+ return String(label);
215
+ const raw = getNestedValue(row, col.key);
216
+ return raw !== undefined && raw !== null ? String(raw) : '';
217
+ };
218
+ /**
219
+ * Renders a resolved FK relation as a clean, truncated chip. Reads the
220
+ * backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
221
+ * its `label`. Falls back to the raw id when no sibling was resolved, and to an
222
+ * empty marker when there is no value at all. Domain-agnostic: works for every
223
+ * `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
224
+ */
225
+ const RelationCell = ({ col, row }) => {
226
+ const isDark = useIsDarkTheme();
227
+ const display = resolveRelationLabel(col, row);
228
+ if (!display)
229
+ return _jsx(EmptyCell, {});
230
+ // Deterministic, SUBTLE color keyed on the resolved label — lighter than
231
+ // enum badges (soft tint, no heavy fill) so category/brand chips read as
232
+ // alive yet stay visually distinct from option/status badges. Inline style
233
+ // (hex-derived) bypasses the host tailwind safelist.
234
+ const chipStyles = relationChipStyles(display, { isDark });
235
+ return (_jsx("span", { className: "inline-flex max-w-[220px] items-center truncate rounded-md px-2 py-0.5 text-sm font-medium", style: chipStyles, title: display, children: _jsx("span", { className: "truncate", children: display }) }));
236
+ };
182
237
  /**
183
238
  * Generic avatar-style cell: round/rounded photo (or initials fallback) +
184
239
  * primary name + optional subtitle. Backs the `avatar`/`search` columns as
@@ -277,6 +332,30 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
277
332
  const styles = generateBadgeStyles(statusColorFor(sv), { isDark });
278
333
  return (_jsx(Badge, { variant: "outline", className: "border-0 capitalize", style: styles, children: sv }));
279
334
  }
335
+ // Resolved FK relation chip. Triggers on an explicit
336
+ // `cellStyle: 'relation'` or on any column carrying a `ref`
337
+ // (a belongs_to FK) that isn't being rendered as an
338
+ // option/badge. Reads the backend-resolved
339
+ // `row[<key w/o _id>] = { value, label }` sibling.
340
+ if (renderAs === 'relation' ||
341
+ (col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')) {
342
+ return _jsx(RelationCell, { col: col, row: row.original });
343
+ }
344
+ // Option/type column: a `select`-style column ships its
345
+ // localized `options: [{value,label,color,icon}]` inline and
346
+ // the cell value is the raw option value (e.g. "storable").
347
+ // Render the matched option's label as a colored badge —
348
+ // same OptionBadge the `badge`/`status` cells use.
349
+ if ((renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
350
+ col.options &&
351
+ col.options.length > 0) {
352
+ if (!value && value !== 0)
353
+ return _jsx(EmptyCell, {});
354
+ const option = col.options.find((o) => o.value === String(value));
355
+ if (option)
356
+ return _jsx(OptionBadge, { option: option, fallback: String(value) });
357
+ return _jsx(Badge, { variant: "outline", children: String(value) });
358
+ }
280
359
  switch (renderAs) {
281
360
  case 'date': {
282
361
  if (!value)
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CAixBnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CA+xBnB"}
@@ -229,8 +229,15 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
229
229
  if (!meta)
230
230
  return;
231
231
  const columnEndpoints = meta.columns.filter(c => c.useOptions && c.searchEndpoint).map(c => c.searchEndpoint);
232
- const filterEndpoints = (meta.filters || []).filter(f => f.searchEndpoint && (f.type === 'select' || f.type === 'boolean')).map(f => f.searchEndpoint);
233
- const allEndpoints = [...columnEndpoints, ...filterEndpoints];
232
+ const filterEndpoints = (meta.filters || []).filter(f => f.searchEndpoint && (f.type === 'select' || f.type === 'dynamic_select' || f.type === 'boolean')).map(f => f.searchEndpoint);
233
+ // Relation (`ref`/`dynamic_select`) columns flagged `filterable`
234
+ // also need their options preloaded so the per-column multi-select
235
+ // combobox has something to show. Mirrors the explicit-filter path
236
+ // above for columns that drive their filter off the column def.
237
+ const columnFilterEndpoints = meta.columns
238
+ .filter(c => c.filterable && c.searchEndpoint)
239
+ .map(c => c.searchEndpoint);
240
+ const allEndpoints = [...columnEndpoints, ...filterEndpoints, ...columnFilterEndpoints];
234
241
  if (allEndpoints.length > 0) {
235
242
  prefetchOptions(allEndpoints).then(fetchedMap => {
236
243
  const colMap = new Map();
@@ -238,16 +245,18 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
238
245
  colMap.set(ep, fetchedMap.get(ep)); });
239
246
  setOptionsMap(colMap);
240
247
  const fMap = new Map();
241
- filterEndpoints.forEach(ep => {
242
- if (fetchedMap.has(ep)) {
243
- fMap.set(ep, (fetchedMap.get(ep) || []).map((item) => ({
244
- label: item.label || item.name || '',
245
- value: String(item.value ?? item.id ?? ''),
246
- icon: item.icon,
247
- color: item.color || item.class,
248
- })));
249
- }
250
- });
248
+ const projectFilterOptions = (ep) => {
249
+ if (!fetchedMap.has(ep) || fMap.has(ep))
250
+ return;
251
+ fMap.set(ep, (fetchedMap.get(ep) || []).map((item) => ({
252
+ label: item.label || item.name || '',
253
+ value: String(item.value ?? item.id ?? ''),
254
+ icon: item.icon,
255
+ color: item.color || item.class,
256
+ })));
257
+ };
258
+ filterEndpoints.forEach(projectFilterOptions);
259
+ columnFilterEndpoints.forEach(projectFilterOptions);
251
260
  setFilterOptionsMap(fMap);
252
261
  });
253
262
  }
@@ -490,14 +499,22 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
490
499
  continue;
491
500
  const hasStaticOptions = (c.options?.length ?? 0) > 0;
492
501
  const hasEndpoint = !!c.searchEndpoint;
493
- // Pick the filter UI from column type:
494
- // - explicit options or searchEndpoint multi-select dropdown
502
+ const isRelation = !!c.ref || c.filterType === 'dynamic_select';
503
+ // Pick the filter UI. The backend's explicit `filterType` wins; when
504
+ // absent we infer it from the column shape:
505
+ // - ref/dynamic_select column → relation multi-select
506
+ // (options stream from searchEndpoint = /options/<ref>)
507
+ // - inline options or searchEndpoint → static multi-select
495
508
  // - boolean → boolean toggle (renders as select under the hood)
496
509
  // - number / number_range / numeric → number range
497
510
  // - date → date range picker (start/end calendar)
498
511
  // - everything else (text, email, phone, tags…) → text contains
499
- let filterType = 'select';
500
- if (hasStaticOptions || hasEndpoint)
512
+ let filterType;
513
+ if (c.filterType)
514
+ filterType = c.filterType;
515
+ else if (isRelation && hasEndpoint)
516
+ filterType = 'dynamic_select';
517
+ else if (hasStaticOptions || hasEndpoint)
501
518
  filterType = 'select';
502
519
  else if (c.type === 'boolean')
503
520
  filterType = 'boolean';
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwa
16
16
  export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, type HotSwapReloadStrategy, type HotSwapReloadConfig, type HotSwapReloadAction, type HotSwapReloadDeps, type UseHotSwapReloadResult, } from './hotswap-reload-policy';
17
17
  export * from './dynamic-icon';
18
18
  export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
19
- export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, type DynamicColumnsHelpers, } from './dynamic-columns';
19
+ export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, type DynamicColumnsHelpers, } from './dynamic-columns';
20
20
  export { DynamicRecordDialog } from './dialogs/dynamic-record';
21
21
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
22
22
  export { ViewRecordDialog } from './dialogs/view-record-dialog';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ export * from './metadata-cache';
20
20
  export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, } from './manifest-hotswap-subscriber';
21
21
  export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, } from './hotswap-reload-policy';
22
22
  export * from './dynamic-icon';
23
- export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, } from './dynamic-columns';
23
+ export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, } from './dynamic-columns';
24
24
  export { DynamicRecordDialog } from './dialogs/dynamic-record';
25
25
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
26
26
  export { ViewRecordDialog } from './dialogs/view-record-dialog';
package/dist/types.d.ts CHANGED
@@ -51,7 +51,13 @@ export interface RelationMeta {
51
51
  export interface FilterDefinition {
52
52
  key: string;
53
53
  label: string;
54
- type: 'select' | 'boolean' | 'date_range' | 'number_range' | 'text';
54
+ /**
55
+ * `dynamic_select` resolves its options server-side from a relation
56
+ * (`searchEndpoint = /options/<ref>`) and renders the same multi-value
57
+ * combobox as `select`. The host loads + caches the options before they
58
+ * surface in the dropdown.
59
+ */
60
+ type: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text';
55
61
  column: string;
56
62
  options?: {
57
63
  value: string | boolean;
@@ -75,9 +81,17 @@ export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {}
75
81
  export interface ColumnDefinition {
76
82
  key: string;
77
83
  label: string;
78
- type: 'text' | 'number' | 'date' | 'select' | 'search' | 'relation-badge-list' | 'avatar' | 'boolean' | 'phone' | 'media-gallery' | 'image' | 'url' | 'link' | 'email' | 'currency' | 'percent' | 'progress' | 'badge' | 'status' | 'tags' | 'color' | 'code' | 'truncate-text' | 'creator' | 'user';
84
+ type: 'text' | 'number' | 'date' | 'select' | 'search' | 'relation-badge-list' | 'avatar' | 'boolean' | 'phone' | 'media-gallery' | 'image' | 'url' | 'link' | 'email' | 'currency' | 'percent' | 'progress' | 'badge' | 'status' | 'tags' | 'color' | 'code' | 'truncate-text' | 'creator' | 'user' | 'relation';
79
85
  sortable: boolean;
80
86
  filterable: boolean;
87
+ /**
88
+ * Explicit filter UI the backend wants for this column when `filterable`.
89
+ * When absent the SDK infers it from the column shape (options/endpoint →
90
+ * `select`, boolean/number/date → their range pickers, else `text`). A
91
+ * `ref` (belongs_to FK) column is served as `dynamic_select` so its options
92
+ * stream from `searchEndpoint = /options/<ref>` into a multi-value combobox.
93
+ */
94
+ filterType?: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text';
81
95
  hidden?: boolean;
82
96
  /**
83
97
  * Scopes where this column is rendered. When `'modal'` (or `'list'`) the
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.10.2",
3
+ "version": "15.0.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "date-fns": ">=3",
35
35
  "react-day-picker": ">=8",
36
36
  "@asteby/metacore-sdk": "^3.1.0",
37
- "@asteby/metacore-ui": "^2.1.2"
37
+ "@asteby/metacore-ui": "^2.3.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@tanstack/react-router": {
@@ -61,8 +61,8 @@
61
61
  "typescript": "^6.0.0",
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
- "@asteby/metacore-ui": "2.1.2",
65
- "@asteby/metacore-sdk": "3.1.0"
64
+ "@asteby/metacore-sdk": "3.1.0",
65
+ "@asteby/metacore-ui": "2.3.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,61 @@
1
+ // Determinism + spread coverage for the curated option-color palette that
2
+ // makes option/relation badges "alive" instead of dead gray. These utilities
3
+ // live in @asteby/metacore-ui/lib and are consumed by the OptionBadge and
4
+ // RelationCell renderers in dynamic-columns.tsx.
5
+ import { describe, it, expect } from 'vitest'
6
+ import { optionColor, OPTION_PALETTE } from '@asteby/metacore-ui/lib'
7
+
8
+ describe('optionColor', () => {
9
+ it('is deterministic — same input always yields the same color', () => {
10
+ const a = optionColor('storable')
11
+ const b = optionColor('storable')
12
+ expect(a).toBe(b)
13
+ // Stable across many repeats (no Math.random / time dependence).
14
+ for (let i = 0; i < 50; i++) {
15
+ expect(optionColor('in_progress')).toBe(optionColor('in_progress'))
16
+ }
17
+ })
18
+
19
+ it('normalizes case/whitespace so equal words collapse to one color', () => {
20
+ expect(optionColor('Active')).toBe(optionColor('active'))
21
+ expect(optionColor(' active ')).toBe(optionColor('active'))
22
+ })
23
+
24
+ it('always returns a color from the curated palette', () => {
25
+ const keys = [
26
+ 'storable', 'consumable', 'service', 'active', 'inactive', 'pending',
27
+ 'frenos', 'llantas', 'suspension', 'aceite', 'filtros', 'baterias',
28
+ 'uuid-1234', 'category', 'brand', 'supplier', 'warehouse', '',
29
+ ]
30
+ for (const key of keys) {
31
+ expect(OPTION_PALETTE).toContain(optionColor(key))
32
+ }
33
+ })
34
+
35
+ it('returns a 6-digit hex with no leading #', () => {
36
+ expect(optionColor('anything')).toMatch(/^[0-9a-f]{6}$/)
37
+ })
38
+
39
+ it('falls back to a stable palette color for empty/nullish input', () => {
40
+ expect(optionColor('')).toBe(OPTION_PALETTE[0])
41
+ // @ts-expect-error exercising the nullish guard
42
+ expect(optionColor(undefined)).toBe(OPTION_PALETTE[0])
43
+ })
44
+
45
+ it('spreads distinct inputs across the palette (not one color for all)', () => {
46
+ const inputs = Array.from({ length: 200 }, (_, i) => `option_${i}`)
47
+ const used = new Set(inputs.map(optionColor))
48
+ // With 16 hues and 200 distinct keys we expect broad coverage; require
49
+ // at least half the palette to be exercised so a degenerate hash that
50
+ // collapses everything is caught.
51
+ expect(used.size).toBeGreaterThanOrEqual(OPTION_PALETTE.length / 2)
52
+ })
53
+
54
+ it('different similar inputs do not all collide to one hue', () => {
55
+ const c1 = optionColor('red')
56
+ const c2 = optionColor('blue')
57
+ const c3 = optionColor('green')
58
+ const distinct = new Set([c1, c2, c3])
59
+ expect(distinct.size).toBeGreaterThan(1)
60
+ })
61
+ })
@@ -0,0 +1,53 @@
1
+ // Pure-logic coverage for the relation/option cell resolution path used by
2
+ // `defaultGetDynamicColumns`. The renderers themselves are JSX (covered in the
3
+ // host's render tests); here we lock the value-resolution contract that drives
4
+ // them so a backend shape change is caught without a DOM.
5
+ import { describe, it, expect } from 'vitest'
6
+ import { relationKeyFor, resolveRelationLabel } from '../dynamic-columns'
7
+ import type { ColumnDefinition } from '../types'
8
+
9
+ const col = (over: Partial<ColumnDefinition>): ColumnDefinition => ({
10
+ key: 'category_id',
11
+ label: 'Categoría',
12
+ type: 'text',
13
+ sortable: true,
14
+ filterable: true,
15
+ ...over,
16
+ })
17
+
18
+ describe('relationKeyFor', () => {
19
+ it('strips the trailing _id (FK key → relation sibling key)', () => {
20
+ expect(relationKeyFor({ key: 'category_id' })).toBe('category')
21
+ expect(relationKeyFor({ key: 'supplier_id' })).toBe('supplier')
22
+ })
23
+
24
+ it('leaves keys without _id untouched', () => {
25
+ expect(relationKeyFor({ key: 'category' })).toBe('category')
26
+ expect(relationKeyFor({ key: 'parent_uid' })).toBe('parent_uid')
27
+ })
28
+ })
29
+
30
+ describe('resolveRelationLabel', () => {
31
+ it('prefers the backend-resolved sibling label', () => {
32
+ const row = {
33
+ category_id: 'uuid-1',
34
+ category: { value: 'uuid-1', label: 'Llantas' },
35
+ }
36
+ expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('Llantas')
37
+ })
38
+
39
+ it('accepts a sibling that uses { name } instead of { label }', () => {
40
+ const row = { category_id: 'uuid-2', category: { value: 'uuid-2', name: 'Frenos' } }
41
+ expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('Frenos')
42
+ })
43
+
44
+ it('falls back to the raw id when no sibling was resolved', () => {
45
+ const row = { category_id: 'uuid-3' }
46
+ expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('uuid-3')
47
+ })
48
+
49
+ it('returns empty string when there is no value at all', () => {
50
+ expect(resolveRelationLabel(col({ ref: 'categories' }), {})).toBe('')
51
+ expect(resolveRelationLabel(col({ ref: 'categories' }), { category_id: null })).toBe('')
52
+ })
53
+ })
@@ -3,8 +3,9 @@
3
3
  // badge (static + endpoint-loaded options), avatar/search, creator/user,
4
4
  // phone, date, boolean, relation-badge-list, media-gallery, image, plus the
5
5
  // declarative pro renderers url/link, email, currency, number, percent/
6
- // progress, status, tags, color, code/truncate-text, and a generic text
7
- // fallback. The renderer resolves `cellStyle ?? type` for each column.
6
+ // progress, status, tags, color, code/truncate-text, relation (resolved FK
7
+ // chip), option/select badges, and a generic text fallback. The renderer
8
+ // resolves `cellStyle ?? type` for each column.
8
9
  //
9
10
  // The implementation was previously duplicated across multiple host apps
10
11
  // (~550 LOC each, drifting). It now lives here so a single fix propagates
@@ -34,7 +35,12 @@ import {
34
35
  FilterableColumnHeader,
35
36
  type ColumnFilterMeta,
36
37
  } from '@asteby/metacore-ui/data-table'
37
- import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib'
38
+ import {
39
+ generateBadgeStyles,
40
+ getInitials,
41
+ optionColor,
42
+ relationChipStyles,
43
+ } from '@asteby/metacore-ui/lib'
38
44
  import { Progress } from './dialogs/_primitives'
39
45
  import { OptionsContext } from './options-context'
40
46
  import { DynamicIcon } from './dynamic-icon'
@@ -271,7 +277,12 @@ interface OptionBadgeProps {
271
277
 
272
278
  const OptionBadge: React.FC<OptionBadgeProps> = ({ option }) => {
273
279
  const isDark = useIsDarkTheme()
274
- const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {}
280
+ // Explicit backend color wins; otherwise derive a stable, cohesive color
281
+ // from the option's value (fallback label) so "dead" gray badges come
282
+ // alive. Inline style (hex-derived) so it works regardless of the host's
283
+ // tailwind safelist — addon-arbitrary classes aren't in the host scan.
284
+ const colorSource = option.color || optionColor(option.value || option.label)
285
+ const colorStyles = generateBadgeStyles(colorSource, { isDark })
275
286
  return (
276
287
  <Badge variant="outline" className="flex items-center gap-1 border-0" style={colorStyles}>
277
288
  {option.icon && <DynamicIcon name={option.icon} className="h-3.5 w-3.5" />}
@@ -288,6 +299,65 @@ const BadgeWithEndpointOptions: React.FC<{ endpoint: string; value: any }> = ({
288
299
  return <Badge variant="outline">{String(value)}</Badge>
289
300
  }
290
301
 
302
+ /**
303
+ * Resolves the relation sibling object a backend serves alongside an FK column.
304
+ * For a column keyed `category_id` the data row also carries
305
+ * `row.category = { value, label }` (the FK key with the trailing `_id`
306
+ * stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
307
+ * sibling consumed by the `creator` renderer. Returns the relation key so the
308
+ * cell can read `row[relationKeyFor(col)]`.
309
+ */
310
+ export const relationKeyFor = (col: Pick<ColumnDefinition, 'key'>): string => {
311
+ const k = col.key
312
+ return k.endsWith('_id') ? k.slice(0, -3) : k
313
+ }
314
+
315
+ /**
316
+ * Reads the resolved relation/option label a backend serves for an FK or
317
+ * option column, falling back to the raw value. Pure so the cell renderers and
318
+ * tests share one resolution path:
319
+ * - relation: prefer the sibling `{ value, label }` object's label.
320
+ * - option: prefer the matched `options[].label` (value compared as string).
321
+ * - else: the raw value coerced to string ('' when nullish).
322
+ */
323
+ export const resolveRelationLabel = (col: ColumnDefinition, row: any): string => {
324
+ const sibling = getNestedValue(row, relationKeyFor(col))
325
+ const label =
326
+ sibling && typeof sibling === 'object'
327
+ ? sibling.label ?? sibling.name
328
+ : undefined
329
+ if (label !== undefined && label !== null && label !== '') return String(label)
330
+ const raw = getNestedValue(row, col.key)
331
+ return raw !== undefined && raw !== null ? String(raw) : ''
332
+ }
333
+
334
+ /**
335
+ * Renders a resolved FK relation as a clean, truncated chip. Reads the
336
+ * backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
337
+ * its `label`. Falls back to the raw id when no sibling was resolved, and to an
338
+ * empty marker when there is no value at all. Domain-agnostic: works for every
339
+ * `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
340
+ */
341
+ const RelationCell: React.FC<{ col: ColumnDefinition; row: any }> = ({ col, row }) => {
342
+ const isDark = useIsDarkTheme()
343
+ const display = resolveRelationLabel(col, row)
344
+ if (!display) return <EmptyCell />
345
+ // Deterministic, SUBTLE color keyed on the resolved label — lighter than
346
+ // enum badges (soft tint, no heavy fill) so category/brand chips read as
347
+ // alive yet stay visually distinct from option/status badges. Inline style
348
+ // (hex-derived) bypasses the host tailwind safelist.
349
+ const chipStyles = relationChipStyles(display, { isDark })
350
+ return (
351
+ <span
352
+ className="inline-flex max-w-[220px] items-center truncate rounded-md px-2 py-0.5 text-sm font-medium"
353
+ style={chipStyles}
354
+ title={display}
355
+ >
356
+ <span className="truncate">{display}</span>
357
+ </span>
358
+ )
359
+ }
360
+
291
361
  /**
292
362
  * Generic avatar-style cell: round/rounded photo (or initials fallback) +
293
363
  * primary name + optional subtitle. Backs the `avatar`/`search` columns as
@@ -453,6 +523,34 @@ export function makeDefaultGetDynamicColumns(
453
523
  )
454
524
  }
455
525
 
526
+ // Resolved FK relation chip. Triggers on an explicit
527
+ // `cellStyle: 'relation'` or on any column carrying a `ref`
528
+ // (a belongs_to FK) that isn't being rendered as an
529
+ // option/badge. Reads the backend-resolved
530
+ // `row[<key w/o _id>] = { value, label }` sibling.
531
+ if (
532
+ renderAs === 'relation' ||
533
+ (col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')
534
+ ) {
535
+ return <RelationCell col={col} row={row.original} />
536
+ }
537
+
538
+ // Option/type column: a `select`-style column ships its
539
+ // localized `options: [{value,label,color,icon}]` inline and
540
+ // the cell value is the raw option value (e.g. "storable").
541
+ // Render the matched option's label as a colored badge —
542
+ // same OptionBadge the `badge`/`status` cells use.
543
+ if (
544
+ (renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
545
+ col.options &&
546
+ col.options.length > 0
547
+ ) {
548
+ if (!value && value !== 0) return <EmptyCell />
549
+ const option = col.options.find((o) => o.value === String(value))
550
+ if (option) return <OptionBadge option={option} fallback={String(value)} />
551
+ return <Badge variant="outline">{String(value)}</Badge>
552
+ }
553
+
456
554
  switch (renderAs) {
457
555
  case 'date': {
458
556
  if (!value) return <span className="text-muted-foreground">-</span>
@@ -300,24 +300,32 @@ export function DynamicTable({
300
300
  }
301
301
  if (!meta) return
302
302
  const columnEndpoints = meta.columns.filter(c => c.useOptions && c.searchEndpoint).map(c => c.searchEndpoint!)
303
- const filterEndpoints = (meta.filters || []).filter(f => f.searchEndpoint && (f.type === 'select' || f.type === 'boolean')).map(f => f.searchEndpoint!)
304
- const allEndpoints = [...columnEndpoints, ...filterEndpoints]
303
+ const filterEndpoints = (meta.filters || []).filter(f => f.searchEndpoint && (f.type === 'select' || f.type === 'dynamic_select' || f.type === 'boolean')).map(f => f.searchEndpoint!)
304
+ // Relation (`ref`/`dynamic_select`) columns flagged `filterable`
305
+ // also need their options preloaded so the per-column multi-select
306
+ // combobox has something to show. Mirrors the explicit-filter path
307
+ // above for columns that drive their filter off the column def.
308
+ const columnFilterEndpoints = meta.columns
309
+ .filter(c => c.filterable && c.searchEndpoint)
310
+ .map(c => c.searchEndpoint!)
311
+ const allEndpoints = [...columnEndpoints, ...filterEndpoints, ...columnFilterEndpoints]
305
312
  if (allEndpoints.length > 0) {
306
313
  prefetchOptions(allEndpoints).then(fetchedMap => {
307
314
  const colMap = new Map<string, any[]>()
308
315
  columnEndpoints.forEach(ep => { if (fetchedMap.has(ep)) colMap.set(ep, fetchedMap.get(ep)!) })
309
316
  setOptionsMap(colMap)
310
317
  const fMap = new Map<string, DynamicFilterOption[]>()
311
- filterEndpoints.forEach(ep => {
312
- if (fetchedMap.has(ep)) {
313
- fMap.set(ep, (fetchedMap.get(ep) || []).map((item: any) => ({
314
- label: item.label || item.name || '',
315
- value: String(item.value ?? item.id ?? ''),
316
- icon: item.icon,
317
- color: item.color || item.class,
318
- })))
319
- }
320
- })
318
+ const projectFilterOptions = (ep: string) => {
319
+ if (!fetchedMap.has(ep) || fMap.has(ep)) return
320
+ fMap.set(ep, (fetchedMap.get(ep) || []).map((item: any) => ({
321
+ label: item.label || item.name || '',
322
+ value: String(item.value ?? item.id ?? ''),
323
+ icon: item.icon,
324
+ color: item.color || item.class,
325
+ })))
326
+ }
327
+ filterEndpoints.forEach(projectFilterOptions)
328
+ columnFilterEndpoints.forEach(projectFilterOptions)
321
329
  setFilterOptionsMap(fMap)
322
330
  })
323
331
  }
@@ -531,14 +539,20 @@ export function DynamicTable({
531
539
  if (!c.filterable || map.has(c.key)) continue
532
540
  const hasStaticOptions = (c.options?.length ?? 0) > 0
533
541
  const hasEndpoint = !!c.searchEndpoint
534
- // Pick the filter UI from column type:
535
- // - explicit options or searchEndpoint multi-select dropdown
542
+ const isRelation = !!c.ref || c.filterType === 'dynamic_select'
543
+ // Pick the filter UI. The backend's explicit `filterType` wins; when
544
+ // absent we infer it from the column shape:
545
+ // - ref/dynamic_select column → relation multi-select
546
+ // (options stream from searchEndpoint = /options/<ref>)
547
+ // - inline options or searchEndpoint → static multi-select
536
548
  // - boolean → boolean toggle (renders as select under the hood)
537
549
  // - number / number_range / numeric → number range
538
550
  // - date → date range picker (start/end calendar)
539
551
  // - everything else (text, email, phone, tags…) → text contains
540
- let filterType: ColumnFilterConfig['filterType'] = 'select'
541
- if (hasStaticOptions || hasEndpoint) filterType = 'select'
552
+ let filterType: ColumnFilterConfig['filterType']
553
+ if (c.filterType) filterType = c.filterType
554
+ else if (isRelation && hasEndpoint) filterType = 'dynamic_select'
555
+ else if (hasStaticOptions || hasEndpoint) filterType = 'select'
542
556
  else if (c.type === 'boolean') filterType = 'boolean'
543
557
  else if (c.type === 'number') filterType = 'number_range'
544
558
  else if (c.type === 'date') filterType = 'date_range'
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export type {
62
62
  export {
63
63
  defaultGetDynamicColumns,
64
64
  makeDefaultGetDynamicColumns,
65
+ relationKeyFor,
66
+ resolveRelationLabel,
65
67
  type DynamicColumnsHelpers,
66
68
  } from './dynamic-columns'
67
69
  export { DynamicRecordDialog } from './dialogs/dynamic-record'
package/src/types.ts CHANGED
@@ -56,7 +56,13 @@ export interface RelationMeta {
56
56
  export interface FilterDefinition {
57
57
  key: string
58
58
  label: string
59
- type: 'select' | 'boolean' | 'date_range' | 'number_range' | 'text'
59
+ /**
60
+ * `dynamic_select` resolves its options server-side from a relation
61
+ * (`searchEndpoint = /options/<ref>`) and renders the same multi-value
62
+ * combobox as `select`. The host loads + caches the options before they
63
+ * surface in the dropdown.
64
+ */
65
+ type: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text'
60
66
  column: string
61
67
  options?: { value: string | boolean; label: string; icon?: string; color?: string }[]
62
68
  searchEndpoint?: string
@@ -104,8 +110,21 @@ export interface ColumnDefinition {
104
110
  | 'truncate-text'
105
111
  | 'creator'
106
112
  | 'user'
113
+ // Resolved FK relation chip. The data row carries a sibling
114
+ // `{ value, label }` object keyed by the column key with the trailing
115
+ // `_id` stripped (e.g. `category_id` → `row.category`). Also triggered
116
+ // implicitly whenever the column carries a `ref` (belongs_to FK).
117
+ | 'relation'
107
118
  sortable: boolean
108
119
  filterable: boolean
120
+ /**
121
+ * Explicit filter UI the backend wants for this column when `filterable`.
122
+ * When absent the SDK infers it from the column shape (options/endpoint →
123
+ * `select`, boolean/number/date → their range pickers, else `text`). A
124
+ * `ref` (belongs_to FK) column is served as `dynamic_select` so its options
125
+ * stream from `searchEndpoint = /options/<ref>` into a multi-value combobox.
126
+ */
127
+ filterType?: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text'
109
128
  hidden?: boolean
110
129
  /**
111
130
  * Scopes where this column is rendered. When `'modal'` (or `'list'`) the