@asteby/metacore-runtime-react 13.10.1 → 14.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,69 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 14.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6299af7: Pro dynamic-table cells + relation/option multi-select filters
8
+
9
+ `DynamicTable` now renders resolved FK relations and option/type columns, and
10
+ filters them server-side — generically, for every declarative addon.
11
+
12
+ **Cells (`dynamic-columns.tsx`)**
13
+ - `relation` renderer: a column carrying a `ref` (belongs_to FK) or
14
+ `cellStyle: 'relation'` renders the backend-resolved sibling
15
+ `row[<key without _id>] = { value, label }` as a clean truncated chip
16
+ (e.g. `category_id` → `row.category.label`). Falls back to the raw id, then
17
+ to an empty marker. Mirrors how `created_by` ships as a `{ name, avatar }`
18
+ sibling for the `creator` renderer.
19
+ - option/type badge: a `select`-style column shipping inline localized
20
+ `options: [{ value, label, color, icon }]` renders the matched option's label
21
+ as a colored `OptionBadge` (e.g. `product_type: "storable"` → the
22
+ "Almacenable" badge), reusing the same badge path as `badge`/`status`.
23
+
24
+ **Filters (`dynamic-table.tsx` + `FilterableColumnHeader`)**
25
+ - New `dynamic_select` filter type: a `filterable` `ref` column loads its
26
+ options from `searchEndpoint = /options/<ref>` (prefetched + cached into
27
+ `filterOptionsMap`) and renders the same multi-value checkbox combobox as
28
+ `select`. The backend's explicit `column.filterType` wins; otherwise it is
29
+ inferred from the column shape.
30
+ - `select` and `dynamic_select` filters support MULTIPLE selected values
31
+ (already Set-based in the header; the gate/active-count/loading states were
32
+ generalized to cover `dynamic_select`).
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies [6299af7]
37
+ - @asteby/metacore-ui@2.2.0
38
+
39
+ ## 13.10.2
40
+
41
+ ### Patch Changes
42
+
43
+ - 2813c61: fix(native-form): render rich widgets from column metadata (ref→searchable picker, image/upload→dropzone)
44
+
45
+ The native create/edit modal (`DynamicRecordDialog`, the one `CreateRecordDialog`
46
+ wraps and fetches `/metadata/modal/:model` for) only routed FK columns to its
47
+ searchable picker when they shipped a legacy `searchEndpoint` / `type: "search"`.
48
+ Now that the kernel serves a belongs_to column's `ref` (and an explicit
49
+ `widget`) on modal fields, a plain `ref` column degraded to a raw uuid text input.
50
+
51
+ `EditField` now honors:
52
+ - `field.ref` (or the snake_case `source`/`relation` aliases, or
53
+ `widget: "dynamic_select"`) → renders `DynamicSelectField`: an async typeahead
54
+ against `/api/options/<ref>?field=id` with option thumbnails when the remote
55
+ rows carry an `image` (e.g. a brand logo). Static inline `options` still take
56
+ the enum `<Select>` path — a `ref` column ships no inline options, so the FK
57
+ branch never shadows a static enum.
58
+ - `widget: "upload"` (alongside the existing `type: "image"`) → the themed file
59
+ dropzone, same control as the Brand logo.
60
+
61
+ Also fixes `deriveRelationFormFields` (the column→field mapper for
62
+ `DynamicRelation` inline child forms): it now carries `col.ref` through to the
63
+ field so a belongs_to column resolves to `dynamic_select`, and maps
64
+ `image`/`media-gallery` columns to media field types so they resolve to the
65
+ `upload` widget instead of a text input.
66
+
3
67
  ## 13.10.1
4
68
 
5
69
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAmE1C,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,GACT,EAAE,wBAAwB,2CAsP1B"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAwF1C,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,GACT,EAAE,wBAAwB,2CAsP1B"}
@@ -12,6 +12,8 @@ import { format, parseISO } from 'date-fns';
12
12
  import { es } from 'date-fns/locale';
13
13
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
14
14
  import { useApi } from '../api-context';
15
+ import { DynamicSelectField } from '../dynamic-select-field';
16
+ import { getFieldRef } from '../dynamic-form-schema';
15
17
  function resolvePath(obj, path) {
16
18
  return path.split('.').reduce((acc, part) => acc?.[part], obj);
17
19
  }
@@ -260,9 +262,21 @@ function EditField({ field, value, onChange }) {
260
262
  if (field.type === 'textarea') {
261
263
  return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
262
264
  }
263
- if (field.type === 'image') {
265
+ // Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
266
+ // `image` type) for a file/photo column. Both render the themed dropzone
267
+ // that POSTs to the host upload endpoint — same control as the Brand logo.
268
+ if (field.type === 'image' || field.widget === 'upload') {
264
269
  return _jsx(ImageUploadField, { field: field, value: value, onChange: onChange });
265
270
  }
271
+ // FK columns: a `ref` (kernel-derived belongs_to target) or an explicit
272
+ // `widget: 'dynamic_select'` renders the async searchable picker against
273
+ // /api/options/<ref>?field=id — with option thumbnails when the remote rows
274
+ // carry an `image` — instead of a raw FK uuid text input. Static inline
275
+ // `options` are handled by the enum <Select> branch below; a ref column does
276
+ // not ship inline options, so this never shadows a static enum.
277
+ if ((getFieldRef(field) || field.widget === 'dynamic_select') && !field.options?.length) {
278
+ return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
279
+ }
266
280
  if (field.type === 'search' && field.searchEndpoint) {
267
281
  return _jsx(SearchField, { field: field, value: value, onChange: onChange });
268
282
  }
@@ -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":"AAyCA,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;AAmHD;;;;;;;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;AA0DD;;;;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
@@ -179,6 +180,49 @@ const BadgeWithEndpointOptions = ({ endpoint, value }) => {
179
180
  return _jsx(OptionBadge, { option: option, fallback: String(value) });
180
181
  return _jsx(Badge, { variant: "outline", children: String(value) });
181
182
  };
183
+ /**
184
+ * Resolves the relation sibling object a backend serves alongside an FK column.
185
+ * For a column keyed `category_id` the data row also carries
186
+ * `row.category = { value, label }` (the FK key with the trailing `_id`
187
+ * stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
188
+ * sibling consumed by the `creator` renderer. Returns the relation key so the
189
+ * cell can read `row[relationKeyFor(col)]`.
190
+ */
191
+ export const relationKeyFor = (col) => {
192
+ const k = col.key;
193
+ return k.endsWith('_id') ? k.slice(0, -3) : k;
194
+ };
195
+ /**
196
+ * Reads the resolved relation/option label a backend serves for an FK or
197
+ * option column, falling back to the raw value. Pure so the cell renderers and
198
+ * tests share one resolution path:
199
+ * - relation: prefer the sibling `{ value, label }` object's label.
200
+ * - option: prefer the matched `options[].label` (value compared as string).
201
+ * - else: the raw value coerced to string ('' when nullish).
202
+ */
203
+ export const resolveRelationLabel = (col, row) => {
204
+ const sibling = getNestedValue(row, relationKeyFor(col));
205
+ const label = sibling && typeof sibling === 'object'
206
+ ? sibling.label ?? sibling.name
207
+ : undefined;
208
+ if (label !== undefined && label !== null && label !== '')
209
+ return String(label);
210
+ const raw = getNestedValue(row, col.key);
211
+ return raw !== undefined && raw !== null ? String(raw) : '';
212
+ };
213
+ /**
214
+ * Renders a resolved FK relation as a clean, truncated chip. Reads the
215
+ * backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
216
+ * its `label`. Falls back to the raw id when no sibling was resolved, and to an
217
+ * empty marker when there is no value at all. Domain-agnostic: works for every
218
+ * `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
219
+ */
220
+ const RelationCell = ({ col, row }) => {
221
+ const display = resolveRelationLabel(col, row);
222
+ if (!display)
223
+ return _jsx(EmptyCell, {});
224
+ return (_jsx("span", { className: "inline-flex max-w-[220px] items-center truncate rounded-md bg-muted px-2 py-0.5 text-sm font-medium text-foreground/80", title: display, children: _jsx("span", { className: "truncate", children: display }) }));
225
+ };
182
226
  /**
183
227
  * Generic avatar-style cell: round/rounded photo (or initials fallback) +
184
228
  * primary name + optional subtitle. Backs the `avatar`/`search` columns as
@@ -277,6 +321,30 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
277
321
  const styles = generateBadgeStyles(statusColorFor(sv), { isDark });
278
322
  return (_jsx(Badge, { variant: "outline", className: "border-0 capitalize", style: styles, children: sv }));
279
323
  }
324
+ // Resolved FK relation chip. Triggers on an explicit
325
+ // `cellStyle: 'relation'` or on any column carrying a `ref`
326
+ // (a belongs_to FK) that isn't being rendered as an
327
+ // option/badge. Reads the backend-resolved
328
+ // `row[<key w/o _id>] = { value, label }` sibling.
329
+ if (renderAs === 'relation' ||
330
+ (col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')) {
331
+ return _jsx(RelationCell, { col: col, row: row.original });
332
+ }
333
+ // Option/type column: a `select`-style column ships its
334
+ // localized `options: [{value,label,color,icon}]` inline and
335
+ // the cell value is the raw option value (e.g. "storable").
336
+ // Render the matched option's label as a colored badge —
337
+ // same OptionBadge the `badge`/`status` cells use.
338
+ if ((renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
339
+ col.options &&
340
+ col.options.length > 0) {
341
+ if (!value && value !== 0)
342
+ return _jsx(EmptyCell, {});
343
+ const option = col.options.find((o) => o.value === String(value));
344
+ if (option)
345
+ return _jsx(OptionBadge, { option: option, fallback: String(value) });
346
+ return _jsx(Badge, { variant: "outline", children: String(value) });
347
+ }
280
348
  switch (renderAs) {
281
349
  case 'date': {
282
350
  if (!value)
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,cAAc,CAAA;AAEhE,MAAM,WAAW,YAAY;IACzB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAGrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3D,UAAU,EAAE,MAAM,GACnB,cAAc,EAAE,CAelB;AAcD;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACnB,MAAM,CAKR;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,MAAM,EAAE,CASV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAU9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC5B;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAYzC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,GAAG,SAAS,GAC5D,MAAM,CAiBR"}
1
+ {"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,cAAc,CAAA;AAEhE,MAAM,WAAW,YAAY;IACzB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAGrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3D,UAAU,EAAE,MAAM,GACnB,cAAc,EAAE,CAoBlB;AAkBD;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACnB,MAAM,CAKR;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,MAAM,EAAE,CASV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAU9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC5B;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAYzC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,GAAG,SAAS,GAC5D,MAAM,CAiBR"}
@@ -59,6 +59,11 @@ export function deriveRelationFormFields(metadata, foreignKey) {
59
59
  type: columnTypeToFieldType(col),
60
60
  required: false,
61
61
  options: col.options?.map(o => ({ value: String(o.value), label: o.label })),
62
+ // Carry the FK target through so a belongs_to column renders the
63
+ // async searchable picker (resolveWidget → 'dynamic_select') rather
64
+ // than a raw uuid text input. Without this the column lost its `ref`
65
+ // crossing the column→field boundary and degraded to plain text.
66
+ ref: col.ref,
62
67
  });
63
68
  }
64
69
  return out;
@@ -69,6 +74,10 @@ function columnTypeToFieldType(col) {
69
74
  case 'boolean': return 'boolean';
70
75
  case 'date': return 'date';
71
76
  case 'select': return 'select';
77
+ // Media columns map to the upload widget (resolveWidget → 'upload') so an
78
+ // image/photo column gets the file dropzone instead of a text input.
79
+ case 'image': return 'image';
80
+ case 'media-gallery': return 'media';
72
81
  case 'text':
73
82
  default:
74
83
  return 'string';
@@ -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":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,YAAY,EAAE,CAAA;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IACzB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,IAAI,EAAE,aAAa,GAAG,cAAc,CAAA;IACpC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EACE,MAAM,GACN,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,qBAAqB,GACrB,QAAQ,GACR,SAAS,GACT,OAAO,GACP,eAAe,GACf,OAAO,GAEP,KAAK,GACL,MAAM,GACN,OAAO,GACP,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,OAAO,GACP,MAAM,GACN,eAAe,GACf,SAAS,GACT,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,gBAAgB,GAChB,QAAQ,GACR,QAAQ,CAAA;AAEd,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC"}
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.1",
3
+ "version": "14.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.2.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@tanstack/react-router": {
@@ -62,7 +62,7 @@
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
64
  "@asteby/metacore-sdk": "3.1.0",
65
- "@asteby/metacore-ui": "2.1.2"
65
+ "@asteby/metacore-ui": "2.2.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -10,6 +10,7 @@ import {
10
10
  pickOptionLabel,
11
11
  relationRowKey,
12
12
  } from '../dynamic-relation-helpers'
13
+ import { resolveWidget } from '../dynamic-form-schema'
13
14
  import type { ColumnDefinition, TableMetadata } from '../types'
14
15
 
15
16
  describe('buildRelationFilterParams', () => {
@@ -134,6 +135,36 @@ describe('deriveRelationFormFields', () => {
134
135
  expect(deriveRelationFormFields(undefined, 'invoice_id')).toEqual([])
135
136
  expect(deriveRelationFormFields({ columns: [] }, 'invoice_id')).toEqual([])
136
137
  })
138
+
139
+ // The column→field boundary must carry `ref` (FK target) so a belongs_to
140
+ // column renders the searchable picker instead of a raw uuid text input.
141
+ it('propaga el ref (FK target) de la columna al field', () => {
142
+ const meta: Pick<TableMetadata, 'columns'> = {
143
+ columns: [
144
+ { key: 'product_id', label: 'Producto', type: 'text', sortable: true, filterable: false, ref: 'product' },
145
+ ],
146
+ }
147
+ const fields = deriveRelationFormFields(meta, 'invoice_id')
148
+ const prod = fields.find(f => f.key === 'product_id')
149
+ expect(prod?.ref).toBe('product')
150
+ // …and that ref drives the widget to the async searchable picker.
151
+ expect(resolveWidget(prod!)).toBe('dynamic_select')
152
+ })
153
+
154
+ // Media columns must map to a media-bearing field type so the form renders
155
+ // the upload dropzone (resolveWidget → 'upload'), not a text input.
156
+ it('mapea columnas image/media-gallery a tipos que resuelven a upload', () => {
157
+ const meta: Pick<TableMetadata, 'columns'> = {
158
+ columns: [
159
+ { key: 'logo', label: 'Logo', type: 'image', sortable: false, filterable: false },
160
+ { key: 'gallery', label: 'Galería', type: 'media-gallery', sortable: false, filterable: false },
161
+ ],
162
+ }
163
+ const fields = deriveRelationFormFields(meta, 'invoice_id')
164
+ const byKey = Object.fromEntries(fields.map(f => [f.key, f]))
165
+ expect(resolveWidget(byKey['logo'])).toBe('upload')
166
+ expect(resolveWidget(byKey['gallery'])).toBe('upload')
167
+ })
137
168
  })
138
169
 
139
170
  describe('relationRowKey', () => {
@@ -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
+ })
@@ -40,6 +40,9 @@ import { format, parseISO } from 'date-fns'
40
40
  import { es } from 'date-fns/locale'
41
41
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react'
42
42
  import { useApi } from '../api-context'
43
+ import { DynamicSelectField } from '../dynamic-select-field'
44
+ import { getFieldRef } from '../dynamic-form-schema'
45
+ import type { ActionFieldDef } from '../types'
43
46
 
44
47
  interface FieldOption {
45
48
  value: string
@@ -58,6 +61,24 @@ interface FieldDef {
58
61
  hidden?: boolean
59
62
  searchEndpoint?: string
60
63
  filterBy?: string
64
+ /**
65
+ * FK target model the kernel auto-derives for a belongs_to column (>=
66
+ * v0.46.x serves it on modal fields, not just action fields). When present
67
+ * the native form renders an async searchable picker (`DynamicSelectField`)
68
+ * against `/api/options/<ref>?field=id` — with option thumbnails when the
69
+ * remote rows carry an `image` — instead of a raw FK text input. Tolerates
70
+ * the snake_case `source`/`relation` aliases the manifest may serve.
71
+ */
72
+ ref?: string
73
+ source?: string
74
+ relation?: string
75
+ /**
76
+ * Explicit renderer hint. Wins over the `type` switch: `dynamic_select`
77
+ * forces the searchable picker, `upload` forces the file dropzone. Lets the
78
+ * kernel opt a plain text/uuid column into a rich widget without changing
79
+ * its SQL type. Unknown values fall through to the `type`-based default.
80
+ */
81
+ widget?: string
61
82
  }
62
83
 
63
84
  // Permissive shape: the wire payload may omit some fields (e.g. `title` is
@@ -566,10 +587,23 @@ function EditField({ field, value, onChange }: {
566
587
  )
567
588
  }
568
589
 
569
- if (field.type === 'image') {
590
+ // Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
591
+ // `image` type) for a file/photo column. Both render the themed dropzone
592
+ // that POSTs to the host upload endpoint — same control as the Brand logo.
593
+ if (field.type === 'image' || field.widget === 'upload') {
570
594
  return <ImageUploadField field={field} value={value} onChange={onChange} />
571
595
  }
572
596
 
597
+ // FK columns: a `ref` (kernel-derived belongs_to target) or an explicit
598
+ // `widget: 'dynamic_select'` renders the async searchable picker against
599
+ // /api/options/<ref>?field=id — with option thumbnails when the remote rows
600
+ // carry an `image` — instead of a raw FK uuid text input. Static inline
601
+ // `options` are handled by the enum <Select> branch below; a ref column does
602
+ // not ship inline options, so this never shadows a static enum.
603
+ if ((getFieldRef(field as ActionFieldDef) || field.widget === 'dynamic_select') && !field.options?.length) {
604
+ return <DynamicSelectField field={field as ActionFieldDef} value={value} onChange={onChange} />
605
+ }
606
+
573
607
  if (field.type === 'search' && field.searchEndpoint) {
574
608
  return <SearchField field={field} value={value} onChange={onChange} />
575
609
  }
@@ -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
@@ -288,6 +289,58 @@ const BadgeWithEndpointOptions: React.FC<{ endpoint: string; value: any }> = ({
288
289
  return <Badge variant="outline">{String(value)}</Badge>
289
290
  }
290
291
 
292
+ /**
293
+ * Resolves the relation sibling object a backend serves alongside an FK column.
294
+ * For a column keyed `category_id` the data row also carries
295
+ * `row.category = { value, label }` (the FK key with the trailing `_id`
296
+ * stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
297
+ * sibling consumed by the `creator` renderer. Returns the relation key so the
298
+ * cell can read `row[relationKeyFor(col)]`.
299
+ */
300
+ export const relationKeyFor = (col: Pick<ColumnDefinition, 'key'>): string => {
301
+ const k = col.key
302
+ return k.endsWith('_id') ? k.slice(0, -3) : k
303
+ }
304
+
305
+ /**
306
+ * Reads the resolved relation/option label a backend serves for an FK or
307
+ * option column, falling back to the raw value. Pure so the cell renderers and
308
+ * tests share one resolution path:
309
+ * - relation: prefer the sibling `{ value, label }` object's label.
310
+ * - option: prefer the matched `options[].label` (value compared as string).
311
+ * - else: the raw value coerced to string ('' when nullish).
312
+ */
313
+ export const resolveRelationLabel = (col: ColumnDefinition, row: any): string => {
314
+ const sibling = getNestedValue(row, relationKeyFor(col))
315
+ const label =
316
+ sibling && typeof sibling === 'object'
317
+ ? sibling.label ?? sibling.name
318
+ : undefined
319
+ if (label !== undefined && label !== null && label !== '') return String(label)
320
+ const raw = getNestedValue(row, col.key)
321
+ return raw !== undefined && raw !== null ? String(raw) : ''
322
+ }
323
+
324
+ /**
325
+ * Renders a resolved FK relation as a clean, truncated chip. Reads the
326
+ * backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
327
+ * its `label`. Falls back to the raw id when no sibling was resolved, and to an
328
+ * empty marker when there is no value at all. Domain-agnostic: works for every
329
+ * `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
330
+ */
331
+ const RelationCell: React.FC<{ col: ColumnDefinition; row: any }> = ({ col, row }) => {
332
+ const display = resolveRelationLabel(col, row)
333
+ if (!display) return <EmptyCell />
334
+ return (
335
+ <span
336
+ className="inline-flex max-w-[220px] items-center truncate rounded-md bg-muted px-2 py-0.5 text-sm font-medium text-foreground/80"
337
+ title={display}
338
+ >
339
+ <span className="truncate">{display}</span>
340
+ </span>
341
+ )
342
+ }
343
+
291
344
  /**
292
345
  * Generic avatar-style cell: round/rounded photo (or initials fallback) +
293
346
  * primary name + optional subtitle. Backs the `avatar`/`search` columns as
@@ -453,6 +506,34 @@ export function makeDefaultGetDynamicColumns(
453
506
  )
454
507
  }
455
508
 
509
+ // Resolved FK relation chip. Triggers on an explicit
510
+ // `cellStyle: 'relation'` or on any column carrying a `ref`
511
+ // (a belongs_to FK) that isn't being rendered as an
512
+ // option/badge. Reads the backend-resolved
513
+ // `row[<key w/o _id>] = { value, label }` sibling.
514
+ if (
515
+ renderAs === 'relation' ||
516
+ (col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')
517
+ ) {
518
+ return <RelationCell col={col} row={row.original} />
519
+ }
520
+
521
+ // Option/type column: a `select`-style column ships its
522
+ // localized `options: [{value,label,color,icon}]` inline and
523
+ // the cell value is the raw option value (e.g. "storable").
524
+ // Render the matched option's label as a colored badge —
525
+ // same OptionBadge the `badge`/`status` cells use.
526
+ if (
527
+ (renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
528
+ col.options &&
529
+ col.options.length > 0
530
+ ) {
531
+ if (!value && value !== 0) return <EmptyCell />
532
+ const option = col.options.find((o) => o.value === String(value))
533
+ if (option) return <OptionBadge option={option} fallback={String(value)} />
534
+ return <Badge variant="outline">{String(value)}</Badge>
535
+ }
536
+
456
537
  switch (renderAs) {
457
538
  case 'date': {
458
539
  if (!value) return <span className="text-muted-foreground">-</span>
@@ -82,6 +82,11 @@ export function deriveRelationFormFields(
82
82
  type: columnTypeToFieldType(col),
83
83
  required: false,
84
84
  options: col.options?.map(o => ({ value: String(o.value), label: o.label })),
85
+ // Carry the FK target through so a belongs_to column renders the
86
+ // async searchable picker (resolveWidget → 'dynamic_select') rather
87
+ // than a raw uuid text input. Without this the column lost its `ref`
88
+ // crossing the column→field boundary and degraded to plain text.
89
+ ref: col.ref,
85
90
  })
86
91
  }
87
92
  return out
@@ -93,6 +98,10 @@ function columnTypeToFieldType(col: ColumnDefinition): string {
93
98
  case 'boolean': return 'boolean'
94
99
  case 'date': return 'date'
95
100
  case 'select': return 'select'
101
+ // Media columns map to the upload widget (resolveWidget → 'upload') so an
102
+ // image/photo column gets the file dropzone instead of a text input.
103
+ case 'image': return 'image'
104
+ case 'media-gallery': return 'media'
96
105
  case 'text':
97
106
  default:
98
107
  return 'string'
@@ -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