@asteby/metacore-runtime-react 13.10.2 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/dynamic-columns.d.ts +19 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +83 -4
- 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 +4 -4
- package/src/__tests__/option-color.test.ts +61 -0
- package/src/__tests__/relation-option-cells.test.ts +53 -0
- package/src/dynamic-columns.tsx +102 -4
- 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,68 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 15.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ab41d75: Make declarative dynamic-table option badges and relation chips feel alive.
|
|
8
|
+
|
|
9
|
+
Options/select/status badges that ship without an explicit `color` from the
|
|
10
|
+
backend now get a deterministic, cohesive color derived from the option value
|
|
11
|
+
(fallback label) instead of rendering as dead gray text. Same value always maps
|
|
12
|
+
to the same hue, and equal words share a color.
|
|
13
|
+
- `@asteby/metacore-ui/lib` adds `optionColor(key)` (curated 16-hue Tailwind-500
|
|
14
|
+
palette, FNV-1a hash, light/dark safe), plus `optionColorBadgeStyles`,
|
|
15
|
+
`relationChipStyles`, and the exported `OPTION_PALETTE`.
|
|
16
|
+
- `OptionBadge` uses the explicit `color` when present, otherwise derives one via
|
|
17
|
+
`optionColor`, and renders the option's lucide `icon` before the label.
|
|
18
|
+
- `RelationCell` (resolved FK chips for category/brand/supplier/…) now gets a
|
|
19
|
+
subtle deterministic color keyed on the related label — kept lighter than enum
|
|
20
|
+
badges (soft tint, no heavy fill) so the two remain distinguishable.
|
|
21
|
+
|
|
22
|
+
All colors come from hex-derived inline styles, so they render correctly
|
|
23
|
+
regardless of the host's tailwind safelist.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [ab41d75]
|
|
28
|
+
- @asteby/metacore-ui@2.3.0
|
|
29
|
+
|
|
30
|
+
## 14.0.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
- 6299af7: Pro dynamic-table cells + relation/option multi-select filters
|
|
35
|
+
|
|
36
|
+
`DynamicTable` now renders resolved FK relations and option/type columns, and
|
|
37
|
+
filters them server-side — generically, for every declarative addon.
|
|
38
|
+
|
|
39
|
+
**Cells (`dynamic-columns.tsx`)**
|
|
40
|
+
- `relation` renderer: a column carrying a `ref` (belongs_to FK) or
|
|
41
|
+
`cellStyle: 'relation'` renders the backend-resolved sibling
|
|
42
|
+
`row[<key without _id>] = { value, label }` as a clean truncated chip
|
|
43
|
+
(e.g. `category_id` → `row.category.label`). Falls back to the raw id, then
|
|
44
|
+
to an empty marker. Mirrors how `created_by` ships as a `{ name, avatar }`
|
|
45
|
+
sibling for the `creator` renderer.
|
|
46
|
+
- option/type badge: a `select`-style column shipping inline localized
|
|
47
|
+
`options: [{ value, label, color, icon }]` renders the matched option's label
|
|
48
|
+
as a colored `OptionBadge` (e.g. `product_type: "storable"` → the
|
|
49
|
+
"Almacenable" badge), reusing the same badge path as `badge`/`status`.
|
|
50
|
+
|
|
51
|
+
**Filters (`dynamic-table.tsx` + `FilterableColumnHeader`)**
|
|
52
|
+
- New `dynamic_select` filter type: a `filterable` `ref` column loads its
|
|
53
|
+
options from `searchEndpoint = /options/<ref>` (prefetched + cached into
|
|
54
|
+
`filterOptionsMap`) and renders the same multi-value checkbox combobox as
|
|
55
|
+
`select`. The backend's explicit `column.filterType` wins; otherwise it is
|
|
56
|
+
inferred from the column shape.
|
|
57
|
+
- `select` and `dynamic_select` filters support MULTIPLE selected values
|
|
58
|
+
(already Set-based in the header; the gate/active-count/loading states were
|
|
59
|
+
generalized to cover `dynamic_select`).
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [6299af7]
|
|
64
|
+
- @asteby/metacore-ui@2.2.0
|
|
65
|
+
|
|
3
66
|
## 13.10.2
|
|
4
67
|
|
|
5
68
|
### Patch Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ColumnDefinition } from './types';
|
|
1
2
|
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
2
3
|
/** Host-supplied helpers consumed by avatar/image cell renderers. */
|
|
3
4
|
export interface DynamicColumnsHelpers {
|
|
@@ -27,6 +28,24 @@ export interface DynamicColumnsHelpers {
|
|
|
27
28
|
* - row with no `status` field → all actions shown.
|
|
28
29
|
*/
|
|
29
30
|
export declare const isActionAllowedForRowState: (action: any, row: any) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the relation sibling object a backend serves alongside an FK column.
|
|
33
|
+
* For a column keyed `category_id` the data row also carries
|
|
34
|
+
* `row.category = { value, label }` (the FK key with the trailing `_id`
|
|
35
|
+
* stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
|
|
36
|
+
* sibling consumed by the `creator` renderer. Returns the relation key so the
|
|
37
|
+
* cell can read `row[relationKeyFor(col)]`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const relationKeyFor: (col: Pick<ColumnDefinition, "key">) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Reads the resolved relation/option label a backend serves for an FK or
|
|
42
|
+
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
43
|
+
* tests share one resolution path:
|
|
44
|
+
* - relation: prefer the sibling `{ value, label }` object's label.
|
|
45
|
+
* - option: prefer the matched `options[].label` (value compared as string).
|
|
46
|
+
* - else: the raw value coerced to string ('' when nullish).
|
|
47
|
+
*/
|
|
48
|
+
export declare const resolveRelationLabel: (col: ColumnDefinition, row: any) => string;
|
|
30
49
|
/**
|
|
31
50
|
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
32
51
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAwHD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAStE,CAAA;AAiED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAgmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
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
|
|
@@ -18,7 +19,7 @@ import * as icons from 'lucide-react';
|
|
|
18
19
|
import { MoreHorizontal } from 'lucide-react';
|
|
19
20
|
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
|
|
20
21
|
import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
|
|
21
|
-
import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib';
|
|
22
|
+
import { generateBadgeStyles, getInitials, optionColor, relationChipStyles, } from '@asteby/metacore-ui/lib';
|
|
22
23
|
import { Progress } from './dialogs/_primitives';
|
|
23
24
|
import { OptionsContext } from './options-context';
|
|
24
25
|
import { DynamicIcon } from './dynamic-icon';
|
|
@@ -168,7 +169,12 @@ const renderRelationBadges = (items, col) => {
|
|
|
168
169
|
};
|
|
169
170
|
const OptionBadge = ({ option }) => {
|
|
170
171
|
const isDark = useIsDarkTheme();
|
|
171
|
-
|
|
172
|
+
// Explicit backend color wins; otherwise derive a stable, cohesive color
|
|
173
|
+
// from the option's value (fallback label) so "dead" gray badges come
|
|
174
|
+
// alive. Inline style (hex-derived) so it works regardless of the host's
|
|
175
|
+
// tailwind safelist — addon-arbitrary classes aren't in the host scan.
|
|
176
|
+
const colorSource = option.color || optionColor(option.value || option.label);
|
|
177
|
+
const colorStyles = generateBadgeStyles(colorSource, { isDark });
|
|
172
178
|
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" }), _jsx("span", { children: option.label })] }));
|
|
173
179
|
};
|
|
174
180
|
const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
@@ -179,6 +185,55 @@ const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
|
179
185
|
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
180
186
|
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
181
187
|
};
|
|
188
|
+
/**
|
|
189
|
+
* Resolves the relation sibling object a backend serves alongside an FK column.
|
|
190
|
+
* For a column keyed `category_id` the data row also carries
|
|
191
|
+
* `row.category = { value, label }` (the FK key with the trailing `_id`
|
|
192
|
+
* stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
|
|
193
|
+
* sibling consumed by the `creator` renderer. Returns the relation key so the
|
|
194
|
+
* cell can read `row[relationKeyFor(col)]`.
|
|
195
|
+
*/
|
|
196
|
+
export const relationKeyFor = (col) => {
|
|
197
|
+
const k = col.key;
|
|
198
|
+
return k.endsWith('_id') ? k.slice(0, -3) : k;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Reads the resolved relation/option label a backend serves for an FK or
|
|
202
|
+
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
203
|
+
* tests share one resolution path:
|
|
204
|
+
* - relation: prefer the sibling `{ value, label }` object's label.
|
|
205
|
+
* - option: prefer the matched `options[].label` (value compared as string).
|
|
206
|
+
* - else: the raw value coerced to string ('' when nullish).
|
|
207
|
+
*/
|
|
208
|
+
export const resolveRelationLabel = (col, row) => {
|
|
209
|
+
const sibling = getNestedValue(row, relationKeyFor(col));
|
|
210
|
+
const label = sibling && typeof sibling === 'object'
|
|
211
|
+
? sibling.label ?? sibling.name
|
|
212
|
+
: undefined;
|
|
213
|
+
if (label !== undefined && label !== null && label !== '')
|
|
214
|
+
return String(label);
|
|
215
|
+
const raw = getNestedValue(row, col.key);
|
|
216
|
+
return raw !== undefined && raw !== null ? String(raw) : '';
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
220
|
+
* backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
|
|
221
|
+
* its `label`. Falls back to the raw id when no sibling was resolved, and to an
|
|
222
|
+
* empty marker when there is no value at all. Domain-agnostic: works for every
|
|
223
|
+
* `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
|
|
224
|
+
*/
|
|
225
|
+
const RelationCell = ({ col, row }) => {
|
|
226
|
+
const isDark = useIsDarkTheme();
|
|
227
|
+
const display = resolveRelationLabel(col, row);
|
|
228
|
+
if (!display)
|
|
229
|
+
return _jsx(EmptyCell, {});
|
|
230
|
+
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
231
|
+
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
232
|
+
// alive yet stay visually distinct from option/status badges. Inline style
|
|
233
|
+
// (hex-derived) bypasses the host tailwind safelist.
|
|
234
|
+
const chipStyles = relationChipStyles(display, { isDark });
|
|
235
|
+
return (_jsx("span", { className: "inline-flex max-w-[220px] items-center truncate rounded-md px-2 py-0.5 text-sm font-medium", style: chipStyles, title: display, children: _jsx("span", { className: "truncate", children: display }) }));
|
|
236
|
+
};
|
|
182
237
|
/**
|
|
183
238
|
* Generic avatar-style cell: round/rounded photo (or initials fallback) +
|
|
184
239
|
* primary name + optional subtitle. Backs the `avatar`/`search` columns as
|
|
@@ -277,6 +332,30 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
277
332
|
const styles = generateBadgeStyles(statusColorFor(sv), { isDark });
|
|
278
333
|
return (_jsx(Badge, { variant: "outline", className: "border-0 capitalize", style: styles, children: sv }));
|
|
279
334
|
}
|
|
335
|
+
// Resolved FK relation chip. Triggers on an explicit
|
|
336
|
+
// `cellStyle: 'relation'` or on any column carrying a `ref`
|
|
337
|
+
// (a belongs_to FK) that isn't being rendered as an
|
|
338
|
+
// option/badge. Reads the backend-resolved
|
|
339
|
+
// `row[<key w/o _id>] = { value, label }` sibling.
|
|
340
|
+
if (renderAs === 'relation' ||
|
|
341
|
+
(col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')) {
|
|
342
|
+
return _jsx(RelationCell, { col: col, row: row.original });
|
|
343
|
+
}
|
|
344
|
+
// Option/type column: a `select`-style column ships its
|
|
345
|
+
// localized `options: [{value,label,color,icon}]` inline and
|
|
346
|
+
// the cell value is the raw option value (e.g. "storable").
|
|
347
|
+
// Render the matched option's label as a colored badge —
|
|
348
|
+
// same OptionBadge the `badge`/`status` cells use.
|
|
349
|
+
if ((renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
|
|
350
|
+
col.options &&
|
|
351
|
+
col.options.length > 0) {
|
|
352
|
+
if (!value && value !== 0)
|
|
353
|
+
return _jsx(EmptyCell, {});
|
|
354
|
+
const option = col.options.find((o) => o.value === String(value));
|
|
355
|
+
if (option)
|
|
356
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
357
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
358
|
+
}
|
|
280
359
|
switch (renderAs) {
|
|
281
360
|
case 'date': {
|
|
282
361
|
if (!value)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,
|
|
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":"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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
|
-
|
|
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": "15.0.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"date-fns": ">=3",
|
|
35
35
|
"react-day-picker": ">=8",
|
|
36
36
|
"@asteby/metacore-sdk": "^3.1.0",
|
|
37
|
-
"@asteby/metacore-ui": "^2.
|
|
37
|
+
"@asteby/metacore-ui": "^2.3.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tanstack/react-router": {
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"typescript": "^6.0.0",
|
|
62
62
|
"vitest": "^4.0.0",
|
|
63
63
|
"zustand": "^5.0.0",
|
|
64
|
-
"@asteby/metacore-
|
|
65
|
-
"@asteby/metacore-
|
|
64
|
+
"@asteby/metacore-sdk": "3.1.0",
|
|
65
|
+
"@asteby/metacore-ui": "2.3.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Determinism + spread coverage for the curated option-color palette that
|
|
2
|
+
// makes option/relation badges "alive" instead of dead gray. These utilities
|
|
3
|
+
// live in @asteby/metacore-ui/lib and are consumed by the OptionBadge and
|
|
4
|
+
// RelationCell renderers in dynamic-columns.tsx.
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { optionColor, OPTION_PALETTE } from '@asteby/metacore-ui/lib'
|
|
7
|
+
|
|
8
|
+
describe('optionColor', () => {
|
|
9
|
+
it('is deterministic — same input always yields the same color', () => {
|
|
10
|
+
const a = optionColor('storable')
|
|
11
|
+
const b = optionColor('storable')
|
|
12
|
+
expect(a).toBe(b)
|
|
13
|
+
// Stable across many repeats (no Math.random / time dependence).
|
|
14
|
+
for (let i = 0; i < 50; i++) {
|
|
15
|
+
expect(optionColor('in_progress')).toBe(optionColor('in_progress'))
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('normalizes case/whitespace so equal words collapse to one color', () => {
|
|
20
|
+
expect(optionColor('Active')).toBe(optionColor('active'))
|
|
21
|
+
expect(optionColor(' active ')).toBe(optionColor('active'))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('always returns a color from the curated palette', () => {
|
|
25
|
+
const keys = [
|
|
26
|
+
'storable', 'consumable', 'service', 'active', 'inactive', 'pending',
|
|
27
|
+
'frenos', 'llantas', 'suspension', 'aceite', 'filtros', 'baterias',
|
|
28
|
+
'uuid-1234', 'category', 'brand', 'supplier', 'warehouse', '',
|
|
29
|
+
]
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
expect(OPTION_PALETTE).toContain(optionColor(key))
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns a 6-digit hex with no leading #', () => {
|
|
36
|
+
expect(optionColor('anything')).toMatch(/^[0-9a-f]{6}$/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('falls back to a stable palette color for empty/nullish input', () => {
|
|
40
|
+
expect(optionColor('')).toBe(OPTION_PALETTE[0])
|
|
41
|
+
// @ts-expect-error exercising the nullish guard
|
|
42
|
+
expect(optionColor(undefined)).toBe(OPTION_PALETTE[0])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('spreads distinct inputs across the palette (not one color for all)', () => {
|
|
46
|
+
const inputs = Array.from({ length: 200 }, (_, i) => `option_${i}`)
|
|
47
|
+
const used = new Set(inputs.map(optionColor))
|
|
48
|
+
// With 16 hues and 200 distinct keys we expect broad coverage; require
|
|
49
|
+
// at least half the palette to be exercised so a degenerate hash that
|
|
50
|
+
// collapses everything is caught.
|
|
51
|
+
expect(used.size).toBeGreaterThanOrEqual(OPTION_PALETTE.length / 2)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('different similar inputs do not all collide to one hue', () => {
|
|
55
|
+
const c1 = optionColor('red')
|
|
56
|
+
const c2 = optionColor('blue')
|
|
57
|
+
const c3 = optionColor('green')
|
|
58
|
+
const distinct = new Set([c1, c2, c3])
|
|
59
|
+
expect(distinct.size).toBeGreaterThan(1)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Pure-logic coverage for the relation/option cell resolution path used by
|
|
2
|
+
// `defaultGetDynamicColumns`. The renderers themselves are JSX (covered in the
|
|
3
|
+
// host's render tests); here we lock the value-resolution contract that drives
|
|
4
|
+
// them so a backend shape change is caught without a DOM.
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { relationKeyFor, resolveRelationLabel } from '../dynamic-columns'
|
|
7
|
+
import type { ColumnDefinition } from '../types'
|
|
8
|
+
|
|
9
|
+
const col = (over: Partial<ColumnDefinition>): ColumnDefinition => ({
|
|
10
|
+
key: 'category_id',
|
|
11
|
+
label: 'Categoría',
|
|
12
|
+
type: 'text',
|
|
13
|
+
sortable: true,
|
|
14
|
+
filterable: true,
|
|
15
|
+
...over,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('relationKeyFor', () => {
|
|
19
|
+
it('strips the trailing _id (FK key → relation sibling key)', () => {
|
|
20
|
+
expect(relationKeyFor({ key: 'category_id' })).toBe('category')
|
|
21
|
+
expect(relationKeyFor({ key: 'supplier_id' })).toBe('supplier')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('leaves keys without _id untouched', () => {
|
|
25
|
+
expect(relationKeyFor({ key: 'category' })).toBe('category')
|
|
26
|
+
expect(relationKeyFor({ key: 'parent_uid' })).toBe('parent_uid')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('resolveRelationLabel', () => {
|
|
31
|
+
it('prefers the backend-resolved sibling label', () => {
|
|
32
|
+
const row = {
|
|
33
|
+
category_id: 'uuid-1',
|
|
34
|
+
category: { value: 'uuid-1', label: 'Llantas' },
|
|
35
|
+
}
|
|
36
|
+
expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('Llantas')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('accepts a sibling that uses { name } instead of { label }', () => {
|
|
40
|
+
const row = { category_id: 'uuid-2', category: { value: 'uuid-2', name: 'Frenos' } }
|
|
41
|
+
expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('Frenos')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('falls back to the raw id when no sibling was resolved', () => {
|
|
45
|
+
const row = { category_id: 'uuid-3' }
|
|
46
|
+
expect(resolveRelationLabel(col({ ref: 'categories' }), row)).toBe('uuid-3')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns empty string when there is no value at all', () => {
|
|
50
|
+
expect(resolveRelationLabel(col({ ref: 'categories' }), {})).toBe('')
|
|
51
|
+
expect(resolveRelationLabel(col({ ref: 'categories' }), { category_id: null })).toBe('')
|
|
52
|
+
})
|
|
53
|
+
})
|
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
|
|
@@ -34,7 +35,12 @@ import {
|
|
|
34
35
|
FilterableColumnHeader,
|
|
35
36
|
type ColumnFilterMeta,
|
|
36
37
|
} from '@asteby/metacore-ui/data-table'
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
generateBadgeStyles,
|
|
40
|
+
getInitials,
|
|
41
|
+
optionColor,
|
|
42
|
+
relationChipStyles,
|
|
43
|
+
} from '@asteby/metacore-ui/lib'
|
|
38
44
|
import { Progress } from './dialogs/_primitives'
|
|
39
45
|
import { OptionsContext } from './options-context'
|
|
40
46
|
import { DynamicIcon } from './dynamic-icon'
|
|
@@ -271,7 +277,12 @@ interface OptionBadgeProps {
|
|
|
271
277
|
|
|
272
278
|
const OptionBadge: React.FC<OptionBadgeProps> = ({ option }) => {
|
|
273
279
|
const isDark = useIsDarkTheme()
|
|
274
|
-
|
|
280
|
+
// Explicit backend color wins; otherwise derive a stable, cohesive color
|
|
281
|
+
// from the option's value (fallback label) so "dead" gray badges come
|
|
282
|
+
// alive. Inline style (hex-derived) so it works regardless of the host's
|
|
283
|
+
// tailwind safelist — addon-arbitrary classes aren't in the host scan.
|
|
284
|
+
const colorSource = option.color || optionColor(option.value || option.label)
|
|
285
|
+
const colorStyles = generateBadgeStyles(colorSource, { isDark })
|
|
275
286
|
return (
|
|
276
287
|
<Badge variant="outline" className="flex items-center gap-1 border-0" style={colorStyles}>
|
|
277
288
|
{option.icon && <DynamicIcon name={option.icon} className="h-3.5 w-3.5" />}
|
|
@@ -288,6 +299,65 @@ const BadgeWithEndpointOptions: React.FC<{ endpoint: string; value: any }> = ({
|
|
|
288
299
|
return <Badge variant="outline">{String(value)}</Badge>
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Resolves the relation sibling object a backend serves alongside an FK column.
|
|
304
|
+
* For a column keyed `category_id` the data row also carries
|
|
305
|
+
* `row.category = { value, label }` (the FK key with the trailing `_id`
|
|
306
|
+
* stripped) — mirroring how `created_by` ships as a `{ name, avatar, email }`
|
|
307
|
+
* sibling consumed by the `creator` renderer. Returns the relation key so the
|
|
308
|
+
* cell can read `row[relationKeyFor(col)]`.
|
|
309
|
+
*/
|
|
310
|
+
export const relationKeyFor = (col: Pick<ColumnDefinition, 'key'>): string => {
|
|
311
|
+
const k = col.key
|
|
312
|
+
return k.endsWith('_id') ? k.slice(0, -3) : k
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Reads the resolved relation/option label a backend serves for an FK or
|
|
317
|
+
* option column, falling back to the raw value. Pure so the cell renderers and
|
|
318
|
+
* tests share one resolution path:
|
|
319
|
+
* - relation: prefer the sibling `{ value, label }` object's label.
|
|
320
|
+
* - option: prefer the matched `options[].label` (value compared as string).
|
|
321
|
+
* - else: the raw value coerced to string ('' when nullish).
|
|
322
|
+
*/
|
|
323
|
+
export const resolveRelationLabel = (col: ColumnDefinition, row: any): string => {
|
|
324
|
+
const sibling = getNestedValue(row, relationKeyFor(col))
|
|
325
|
+
const label =
|
|
326
|
+
sibling && typeof sibling === 'object'
|
|
327
|
+
? sibling.label ?? sibling.name
|
|
328
|
+
: undefined
|
|
329
|
+
if (label !== undefined && label !== null && label !== '') return String(label)
|
|
330
|
+
const raw = getNestedValue(row, col.key)
|
|
331
|
+
return raw !== undefined && raw !== null ? String(raw) : ''
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
336
|
+
* backend-resolved sibling `{ value, label }` (see `relationKeyFor`) and shows
|
|
337
|
+
* its `label`. Falls back to the raw id when no sibling was resolved, and to an
|
|
338
|
+
* empty marker when there is no value at all. Domain-agnostic: works for every
|
|
339
|
+
* `belongs_to` column (category, supplier, warehouse, …) without per-addon code.
|
|
340
|
+
*/
|
|
341
|
+
const RelationCell: React.FC<{ col: ColumnDefinition; row: any }> = ({ col, row }) => {
|
|
342
|
+
const isDark = useIsDarkTheme()
|
|
343
|
+
const display = resolveRelationLabel(col, row)
|
|
344
|
+
if (!display) return <EmptyCell />
|
|
345
|
+
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
346
|
+
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
347
|
+
// alive yet stay visually distinct from option/status badges. Inline style
|
|
348
|
+
// (hex-derived) bypasses the host tailwind safelist.
|
|
349
|
+
const chipStyles = relationChipStyles(display, { isDark })
|
|
350
|
+
return (
|
|
351
|
+
<span
|
|
352
|
+
className="inline-flex max-w-[220px] items-center truncate rounded-md px-2 py-0.5 text-sm font-medium"
|
|
353
|
+
style={chipStyles}
|
|
354
|
+
title={display}
|
|
355
|
+
>
|
|
356
|
+
<span className="truncate">{display}</span>
|
|
357
|
+
</span>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
291
361
|
/**
|
|
292
362
|
* Generic avatar-style cell: round/rounded photo (or initials fallback) +
|
|
293
363
|
* primary name + optional subtitle. Backs the `avatar`/`search` columns as
|
|
@@ -453,6 +523,34 @@ export function makeDefaultGetDynamicColumns(
|
|
|
453
523
|
)
|
|
454
524
|
}
|
|
455
525
|
|
|
526
|
+
// Resolved FK relation chip. Triggers on an explicit
|
|
527
|
+
// `cellStyle: 'relation'` or on any column carrying a `ref`
|
|
528
|
+
// (a belongs_to FK) that isn't being rendered as an
|
|
529
|
+
// option/badge. Reads the backend-resolved
|
|
530
|
+
// `row[<key w/o _id>] = { value, label }` sibling.
|
|
531
|
+
if (
|
|
532
|
+
renderAs === 'relation' ||
|
|
533
|
+
(col.ref && !col.options?.length && renderAs !== 'badge' && renderAs !== 'status')
|
|
534
|
+
) {
|
|
535
|
+
return <RelationCell col={col} row={row.original} />
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Option/type column: a `select`-style column ships its
|
|
539
|
+
// localized `options: [{value,label,color,icon}]` inline and
|
|
540
|
+
// the cell value is the raw option value (e.g. "storable").
|
|
541
|
+
// Render the matched option's label as a colored badge —
|
|
542
|
+
// same OptionBadge the `badge`/`status` cells use.
|
|
543
|
+
if (
|
|
544
|
+
(renderAs === 'select' || renderAs === 'option' || col.type === 'select') &&
|
|
545
|
+
col.options &&
|
|
546
|
+
col.options.length > 0
|
|
547
|
+
) {
|
|
548
|
+
if (!value && value !== 0) return <EmptyCell />
|
|
549
|
+
const option = col.options.find((o) => o.value === String(value))
|
|
550
|
+
if (option) return <OptionBadge option={option} fallback={String(value)} />
|
|
551
|
+
return <Badge variant="outline">{String(value)}</Badge>
|
|
552
|
+
}
|
|
553
|
+
|
|
456
554
|
switch (renderAs) {
|
|
457
555
|
case 'date': {
|
|
458
556
|
if (!value) return <span className="text-muted-foreground">-</span>
|
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
|