@asteby/metacore-runtime-react 13.10.2 → 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,41 @@
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
+
3
39
  ## 13.10.2
4
40
 
5
41
  ### 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":"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-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": "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": {
@@ -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.2.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -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
@@ -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>
@@ -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