@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 +64 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +15 -1
- package/dist/dynamic-columns.d.ts +19 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +70 -2
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +9 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +33 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/dynamic-relation.test.ts +31 -0
- package/src/__tests__/relation-option-cells.test.ts +53 -0
- package/src/dialogs/dynamic-record.tsx +35 -1
- package/src/dynamic-columns.tsx +83 -2
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-table.tsx +30 -16
- package/src/index.ts +2 -0
- package/src/types.ts +20 -1
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;
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -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,
|
|
8
|
-
//
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
242
|
-
if (fetchedMap.has(ep))
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
494
|
-
//
|
|
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
|
|
500
|
-
if (
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,kBAAkB,EAClB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACvB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
|
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
|
-
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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": "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -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,
|
|
7
|
-
//
|
|
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'
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
312
|
-
if (fetchedMap.has(ep))
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
535
|
-
//
|
|
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']
|
|
541
|
-
if (
|
|
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
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
|
-
|
|
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
|