@asteby/metacore-runtime-react 9.1.0 → 9.2.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 +36 -0
- package/dist/dynamic-form-schema.d.ts +5 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +34 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +18 -2
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +59 -22
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +87 -0
- package/dist/use-options-resolver.d.ts.map +1 -0
- package/dist/use-options-resolver.js +147 -0
- package/dist/use-org-config-bridge.d.ts +28 -0
- package/dist/use-org-config-bridge.d.ts.map +1 -0
- package/dist/use-org-config-bridge.js +50 -0
- package/package.json +2 -2
- package/src/__tests__/use-options-resolver.test.ts +127 -0
- package/src/dynamic-form-schema.ts +36 -0
- package/src/dynamic-form.tsx +40 -2
- package/src/dynamic-relation.tsx +55 -20
- package/src/index.ts +15 -0
- package/src/types.ts +24 -0
- package/src/use-options-resolver.ts +232 -0
- package/src/use-org-config-bridge.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 9.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 150a907: feat: useOptionsResolver hook + locale-aware Validation via OrgConfigProvider
|
|
8
|
+
|
|
9
|
+
**runtime-react:**
|
|
10
|
+
- New `useOptionsResolver(args)` hook that consumes the v0.9.0 kernel
|
|
11
|
+
envelope `{ success, data, meta: { type, count } }` from
|
|
12
|
+
`GET /api/options/:model?field=…`. Replaces the ad-hoc `/data/<model>`
|
|
13
|
+
reads `<DynamicRelation>` used to do.
|
|
14
|
+
- `<DynamicForm>` now renders a Ref-driven `<RefSelect>` whenever an
|
|
15
|
+
`ActionFieldDef.ref` is present — apps stop hardcoding option lists for
|
|
16
|
+
belongs_to FKs.
|
|
17
|
+
- `<DynamicRelation>` (kind="many_to_many") prefers the canonical options
|
|
18
|
+
endpoint via `useOptionsResolver`. The legacy `referencesEndpoint` prop
|
|
19
|
+
remains a working escape hatch for apps wired against custom routes.
|
|
20
|
+
- `ColumnDefinition.ref` and `ColumnDefinition.validation` are now part of
|
|
21
|
+
the metadata contract the SDK reads. `ActionFieldDef.ref` joins the
|
|
22
|
+
field-level type so addons can declare ref-aware modal fields.
|
|
23
|
+
- New `setOrgConfigBridge` / `resolveValidatorToken` surface lets apps
|
|
24
|
+
feed a `useOrgConfig`-backed resolver into the SDK's validator
|
|
25
|
+
pipeline. Validators with `custom: '$org.<key>'` are resolved at form
|
|
26
|
+
build time; unresolved tokens degrade to no-op so missing config does
|
|
27
|
+
not crash forms.
|
|
28
|
+
- New `registerValidator(slug, fn)` lets apps install their own
|
|
29
|
+
region-specific validators (e.g. `mx.rfc`, `co.nit`) without leaking
|
|
30
|
+
fiscal vocabulary into the SDK.
|
|
31
|
+
|
|
32
|
+
**app-providers:**
|
|
33
|
+
- New `OrgConfigProvider` + `useOrgConfig()` companion to
|
|
34
|
+
`PlatformConfigProvider`. Apps wire a per-org config fetcher and the
|
|
35
|
+
provider exposes typed `currency`, `locale`, `validators` plus a
|
|
36
|
+
`resolveValidator(refOrKey)` helper for the `$org.<key>` reference
|
|
37
|
+
contract the kernel ≥ v0.9.0 emits.
|
|
38
|
+
|
|
3
39
|
## 9.1.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ActionFieldDef } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Apps register validator implementations by slug. The slug is the value
|
|
5
|
+
* `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerValidator(slug: string, fn: (s: z.ZodString) => z.ZodString): void;
|
|
3
8
|
export declare function buildZodSchema(fields: ActionFieldDef[]): z.ZodObject<{
|
|
4
9
|
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
5
10
|
}, z.core.$strip>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AA4CD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAU3D"}
|
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
// callers (and unit tests) can use the zod schema without pulling in React or
|
|
3
3
|
// metacore-ui primitives.
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { resolveValidatorToken } from './use-org-config-bridge';
|
|
6
|
+
/**
|
|
7
|
+
* Built-in validators the SDK knows how to apply by symbolic name. Apps
|
|
8
|
+
* that wire `OrgConfigProvider` map `$org.<key>` references to one of
|
|
9
|
+
* these slugs (or to a custom slug they register). Unknown slugs are a
|
|
10
|
+
* no-op so unresolved $org references degrade to "no extra check"
|
|
11
|
+
* rather than a runtime crash — matches the kernel's pass-through
|
|
12
|
+
* semantics for unresolved references.
|
|
13
|
+
*/
|
|
14
|
+
const builtinValidators = {
|
|
15
|
+
// The SDK ships ZERO fiscal vocabulary by default. Apps register
|
|
16
|
+
// their own validators (mx.rfc, co.nit, pe.ruc, etc.) via
|
|
17
|
+
// `registerValidator` so kernel/SDK stay region-agnostic.
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Apps register validator implementations by slug. The slug is the value
|
|
21
|
+
* `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
|
|
22
|
+
*/
|
|
23
|
+
export function registerValidator(slug, fn) {
|
|
24
|
+
builtinValidators[slug] = fn;
|
|
25
|
+
}
|
|
26
|
+
function applyCustomValidator(s, customToken) {
|
|
27
|
+
if (!customToken)
|
|
28
|
+
return s;
|
|
29
|
+
const resolved = resolveValidatorToken(customToken);
|
|
30
|
+
if (!resolved)
|
|
31
|
+
return s;
|
|
32
|
+
const fn = builtinValidators[resolved];
|
|
33
|
+
return fn ? fn(s) : s;
|
|
34
|
+
}
|
|
5
35
|
// Builds a zod object schema from an ActionFieldDef[]. Required fields stay
|
|
6
36
|
// non-empty; optional fields accept undefined / "". Validation rules
|
|
7
37
|
// (regex/min/max) layer on top: for numeric columns they bound the value, for
|
|
@@ -46,6 +76,10 @@ function fieldToZod(field) {
|
|
|
46
76
|
s = s.email('Email inválido');
|
|
47
77
|
if (field.type === 'url')
|
|
48
78
|
s = s.url('URL inválida');
|
|
79
|
+
// Custom validator: a literal slug (`mx.rfc`) OR a `$org.<key>`
|
|
80
|
+
// reference resolved through the OrgConfigProvider. Unknown slugs
|
|
81
|
+
// pass through as no-ops so apps never crash on missing config.
|
|
82
|
+
s = applyCustomValidator(s, v.custom);
|
|
49
83
|
if (field.required) {
|
|
50
84
|
return s.min(Math.max(typeof v.min === 'number' ? v.min : 1, 1), `${field.label} es requerido`);
|
|
51
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGrE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AAExC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAgElB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
7
7
|
import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
|
|
8
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
8
9
|
export { buildZodSchema, resolveWidget };
|
|
9
10
|
export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
|
|
10
11
|
const [values, setValues] = useState({});
|
|
@@ -42,10 +43,17 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
42
43
|
setSubmitting(false);
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [fields.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }),
|
|
46
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [fields.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), _jsx(FieldRenderer, { field: field, value: values[field.key], onChange: (v) => update(field.key, v) }), errors[field.key] && (_jsx("span", { className: "text-red-500 text-sm", role: "alert", children: errors[field.key] }))] }, field.key))), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled, children: submitLabel })] })] }));
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
+
function FieldRenderer({ field, value, onChange }) {
|
|
48
49
|
const widget = resolveWidget(field);
|
|
50
|
+
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
51
|
+
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
52
|
+
// the path the kernel auto-derives for FK columns; legacy callers
|
|
53
|
+
// shipping inline `options` keep working in the branch below.
|
|
54
|
+
if (widget === 'select' && field.ref) {
|
|
55
|
+
return _jsx(RefSelect, { field: field, value: value, onChange: onChange });
|
|
56
|
+
}
|
|
49
57
|
switch (widget) {
|
|
50
58
|
case 'textarea':
|
|
51
59
|
return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
|
|
@@ -68,3 +76,11 @@ function renderField(field, value, onChange) {
|
|
|
68
76
|
return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
|
|
69
77
|
}
|
|
70
78
|
}
|
|
79
|
+
function RefSelect({ field, value, onChange }) {
|
|
80
|
+
const { options, loading } = useOptionsResolver({
|
|
81
|
+
modelKey: '', // unused — `ref` drives the URL
|
|
82
|
+
fieldKey: 'id',
|
|
83
|
+
ref: field.ref,
|
|
84
|
+
});
|
|
85
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: loading, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
86
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA0CA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;CACtB;AAiBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAK1D"}
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Plus, Trash2, Pencil } from 'lucide-react';
|
|
|
10
10
|
import { useApi } from './api-context';
|
|
11
11
|
import { useMetadataCache } from './metadata-cache';
|
|
12
12
|
import { DynamicForm } from './dynamic-form';
|
|
13
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
13
14
|
import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
14
15
|
export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
15
16
|
const DEFAULT_STRINGS = {
|
|
@@ -140,33 +141,58 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
140
141
|
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
|
|
141
142
|
const refKey = referencesKey || `${references}_id`;
|
|
142
143
|
const pivotPath = pivotEndpoint || `/data/${through}`;
|
|
143
|
-
|
|
144
|
+
// referencesEndpoint is preserved as a legacy escape hatch — when set
|
|
145
|
+
// we keep the old `/data/<references>` raw fetch path (so apps that
|
|
146
|
+
// depend on a custom server route do not break). When unset we use
|
|
147
|
+
// the canonical `/api/options/:references` endpoint via
|
|
148
|
+
// useOptionsResolver, which is what the kernel auto-derives Ref to.
|
|
149
|
+
const useResolver = !referencesEndpoint;
|
|
150
|
+
const legacyTargetPath = referencesEndpoint || `/data/${references}`;
|
|
144
151
|
const cachedTargetMeta = getMetadata(references);
|
|
145
152
|
const [targetMeta, setTargetMeta] = useState(cachedTargetMeta || null);
|
|
146
153
|
const [targetRows, setTargetRows] = useState([]);
|
|
147
154
|
const [pivotRows, setPivotRows] = useState([]);
|
|
148
155
|
const [loading, setLoading] = useState(true);
|
|
149
156
|
const [syncing, setSyncing] = useState(false);
|
|
150
|
-
|
|
157
|
+
// Canonical path: SDK options resolver. Only fires when no legacy
|
|
158
|
+
// override is set. The hook is a no-op when `useResolver` is false.
|
|
159
|
+
const resolved = useOptionsResolver({
|
|
160
|
+
modelKey: '',
|
|
161
|
+
fieldKey: 'id',
|
|
162
|
+
ref: useResolver ? references : undefined,
|
|
163
|
+
enabled: useResolver,
|
|
164
|
+
});
|
|
165
|
+
const fetchPivotAndMeta = useCallback(async () => {
|
|
151
166
|
setLoading(true);
|
|
152
167
|
try {
|
|
153
168
|
const params = buildRelationFilterParams(foreignKey, parentId);
|
|
154
|
-
const
|
|
155
|
-
targetMeta ? Promise.resolve(null) : api.get(`/metadata/table/${references}`),
|
|
169
|
+
const tasks = [
|
|
156
170
|
api.get(pivotPath, { params }),
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
];
|
|
172
|
+
if (!targetMeta)
|
|
173
|
+
tasks.push(api.get(`/metadata/table/${references}`));
|
|
174
|
+
// Legacy fallback path: the resolver is disabled, fetch the
|
|
175
|
+
// target rows the old way so callers that depend on a custom
|
|
176
|
+
// route keep working.
|
|
177
|
+
if (!useResolver)
|
|
178
|
+
tasks.push(api.get(legacyTargetPath));
|
|
179
|
+
const results = await Promise.all(tasks);
|
|
180
|
+
const pivotRes = results[0];
|
|
181
|
+
if (pivotRes.data.success)
|
|
182
|
+
setPivotRows(pivotRes.data.data || []);
|
|
183
|
+
let cursor = 1;
|
|
184
|
+
if (!targetMeta) {
|
|
185
|
+
const metaRes = results[cursor++];
|
|
186
|
+
if (metaRes.data?.success) {
|
|
187
|
+
setTargetMeta(metaRes.data.data);
|
|
188
|
+
cacheMetadata(references, metaRes.data.data);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!useResolver) {
|
|
192
|
+
const targetRes = results[cursor++];
|
|
193
|
+
if (targetRes.data.success)
|
|
194
|
+
setTargetRows(targetRes.data.data || []);
|
|
163
195
|
}
|
|
164
|
-
const pivotList = pivotRes.data;
|
|
165
|
-
if (pivotList.success)
|
|
166
|
-
setPivotRows(pivotList.data || []);
|
|
167
|
-
const targetList = targetRes.data;
|
|
168
|
-
if (targetList.success)
|
|
169
|
-
setTargetRows(targetList.data || []);
|
|
170
196
|
}
|
|
171
197
|
catch (err) {
|
|
172
198
|
console.error('DynamicRelation m2m fetch error', err);
|
|
@@ -174,16 +200,22 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
174
200
|
finally {
|
|
175
201
|
setLoading(false);
|
|
176
202
|
}
|
|
177
|
-
}, [api, pivotPath,
|
|
178
|
-
useEffect(() => {
|
|
203
|
+
}, [api, pivotPath, foreignKey, parentId, references, targetMeta, cacheMetadata, useResolver, legacyTargetPath]);
|
|
204
|
+
useEffect(() => { fetchPivotAndMeta(); }, [fetchPivotAndMeta]);
|
|
179
205
|
const options = useMemo(() => {
|
|
206
|
+
if (useResolver) {
|
|
207
|
+
return resolved.options.map((o) => ({
|
|
208
|
+
value: String(o.id),
|
|
209
|
+
label: o.label,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
180
212
|
return targetRows
|
|
181
213
|
.filter(r => r && r.id !== undefined && r.id !== null && r.id !== '')
|
|
182
214
|
.map(r => ({
|
|
183
215
|
value: String(r.id),
|
|
184
216
|
label: pickOptionLabel(r, displayKey, targetMeta?.columns),
|
|
185
217
|
}));
|
|
186
|
-
}, [targetRows, displayKey, targetMeta]);
|
|
218
|
+
}, [useResolver, resolved.options, targetRows, displayKey, targetMeta]);
|
|
187
219
|
const selectedIds = useMemo(() => extractSelectedTargetIds(pivotRows, refKey), [pivotRows, refKey]);
|
|
188
220
|
const pivotIndex = useMemo(() => buildPivotRowIndex(pivotRows, refKey), [pivotRows, refKey]);
|
|
189
221
|
const handleChange = useCallback(async (next) => {
|
|
@@ -212,7 +244,12 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
212
244
|
if (!res.data?.success)
|
|
213
245
|
throw new Error('detach failed');
|
|
214
246
|
}
|
|
215
|
-
await
|
|
247
|
+
await fetchPivotAndMeta();
|
|
248
|
+
// Refresh resolver-driven options when active so newly attached
|
|
249
|
+
// targets reflect immediately. Refetching the pivot rows alone
|
|
250
|
+
// is enough when the resolver branch is off.
|
|
251
|
+
if (useResolver)
|
|
252
|
+
resolved.refetch();
|
|
216
253
|
onChange?.();
|
|
217
254
|
}
|
|
218
255
|
catch (err) {
|
|
@@ -221,6 +258,6 @@ function ManyToManyRelation({ kind, through, references, foreignKey, referencesK
|
|
|
221
258
|
finally {
|
|
222
259
|
setSyncing(false);
|
|
223
260
|
}
|
|
224
|
-
}, [api, canCreate, canDelete,
|
|
225
|
-
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-through": through, "data-relation-references": references, children: [labels.title && (_jsx("div", { className: "pb-3", children: _jsx("h3", { className: "text-sm font-medium", children: labels.title }) })), loading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : options.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(MultiSelect, { options: options, selected: selectedIds, onChange: handleChange, placeholder: labels.selectPlaceholder, searchPlaceholder: labels.selectSearchPlaceholder, emptyMessage: labels.selectEmpty }))] }));
|
|
261
|
+
}, [api, canCreate, canDelete, fetchPivotAndMeta, useResolver, resolved, foreignKey, onChange, parentId, pivotIndex, pivotPath, refKey, selectedIds, syncing]);
|
|
262
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-through": through, "data-relation-references": references, children: [labels.title && (_jsx("div", { className: "pb-3", children: _jsx("h3", { className: "text-sm font-medium", children: labels.title }) })), (loading || (useResolver && resolved.loading)) ? (_jsx(Skeleton, { className: "h-10 w-full" })) : options.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(MultiSelect, { options: options, selected: selectedIds, onChange: handleChange, placeholder: labels.selectPlaceholder, searchPlaceholder: labels.selectSearchPlaceholder, emptyMessage: labels.selectEmpty }))] }));
|
|
226
263
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,4 +20,7 @@ export { DynamicCRUDPage, type DynamicCRUDPageProps, type DynamicCRUDPageStrings
|
|
|
20
20
|
export { DynamicRelation, type DynamicRelationProps, type DynamicRelationStrings, type DynamicRelationKind, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
21
21
|
export { registerModelExtension, getModelExtension, clearModelExtensions, type ModelExtension, type ModelExtensionProps, } from './model-extension-registry';
|
|
22
22
|
export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
|
|
23
|
+
export { useOptionsResolver, projectOption, type ResolvedOption, type OptionsMeta, type UseOptionsResolverArgs, type UseOptionsResolverResult, } from './use-options-resolver';
|
|
24
|
+
export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, type OrgConfigBridge, } from './use-org-config-bridge';
|
|
25
|
+
export { registerValidator } from './dynamic-form-schema';
|
|
23
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,cAAc,gBAAgB,CAAA;AAC9B,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,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,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,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,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"}
|
|
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,cAAc,gBAAgB,CAAA;AAC9B,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,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,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,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,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
|
@@ -24,3 +24,6 @@ export { DynamicCRUDPage, } from './dynamic-crud-page';
|
|
|
24
24
|
export { DynamicRelation, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
25
25
|
export { registerModelExtension, getModelExtension, clearModelExtensions, } from './model-extension-registry';
|
|
26
26
|
export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
|
|
27
|
+
export { useOptionsResolver, projectOption, } from './use-options-resolver';
|
|
28
|
+
export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, } from './use-org-config-bridge';
|
|
29
|
+
export { registerValidator } from './dynamic-form-schema';
|
package/dist/types.d.ts
CHANGED
|
@@ -74,6 +74,20 @@ export interface ColumnDefinition {
|
|
|
74
74
|
icon?: string;
|
|
75
75
|
color?: string;
|
|
76
76
|
}[];
|
|
77
|
+
/**
|
|
78
|
+
* FK target model. When the kernel auto-derives this from a
|
|
79
|
+
* belongs_to relation (or an author sets it explicitly), the SDK
|
|
80
|
+
* resolves the column's options against `/api/options/<ref>?field=id`
|
|
81
|
+
* via `useOptionsResolver`. Wins over `searchEndpoint` for select
|
|
82
|
+
* widgets — `searchEndpoint` stays as the legacy escape hatch.
|
|
83
|
+
*/
|
|
84
|
+
ref?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Server-side validation rules the SDK can also pre-flight in the
|
|
87
|
+
* form layer. `custom` may be a literal slug or a $org.<key>
|
|
88
|
+
* reference resolved through the OrgConfigProvider.
|
|
89
|
+
*/
|
|
90
|
+
validation?: FieldValidation;
|
|
77
91
|
}
|
|
78
92
|
export interface ActionCondition {
|
|
79
93
|
field: string;
|
|
@@ -101,6 +115,12 @@ export interface ActionFieldDef {
|
|
|
101
115
|
searchEndpoint?: string;
|
|
102
116
|
validation?: FieldValidation;
|
|
103
117
|
widget?: FieldWidget | string;
|
|
118
|
+
/**
|
|
119
|
+
* FK target model — same semantics as ColumnDefinition.ref. When
|
|
120
|
+
* present, DynamicForm resolves the field's options through
|
|
121
|
+
* `useOptionsResolver` against `/api/options/<ref>?field=id`.
|
|
122
|
+
*/
|
|
123
|
+
ref?: string;
|
|
104
124
|
}
|
|
105
125
|
export interface ActionDefinition {
|
|
106
126
|
key: string;
|
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;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,OAAO,CAAA;IAC3I,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;
|
|
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;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,OAAO,CAAA;IAC3I,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,CAAA;AAEd,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface ResolvedOption {
|
|
2
|
+
/** Canonical id (server-side primary key). */
|
|
3
|
+
id: string | number;
|
|
4
|
+
/** Same as `id` — preserved for legacy frontend parity. */
|
|
5
|
+
value: string | number;
|
|
6
|
+
/** Display string. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Same as `label` — preserved for legacy frontend parity. */
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
image?: string | null;
|
|
12
|
+
color?: string | null;
|
|
13
|
+
icon?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface OptionsMeta {
|
|
16
|
+
/** 'static' for inline options, 'dynamic' for FK-resolved lists. */
|
|
17
|
+
type: 'static' | 'dynamic' | string;
|
|
18
|
+
/** Number of options the server returned in this batch. */
|
|
19
|
+
count: number;
|
|
20
|
+
}
|
|
21
|
+
export interface UseOptionsResolverArgs {
|
|
22
|
+
/**
|
|
23
|
+
* The owning model whose options endpoint is queried. Pass the model
|
|
24
|
+
* key (e.g. 'sales_orders'). Required — passing an empty string puts
|
|
25
|
+
* the hook in idle mode and no fetch fires.
|
|
26
|
+
*/
|
|
27
|
+
modelKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* Field on `modelKey` to resolve. Maps to `?field=<fieldKey>`.
|
|
30
|
+
*/
|
|
31
|
+
fieldKey: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional FK target. When set the hook resolves against
|
|
34
|
+
* `/api/options/<ref>?field=id` instead of `/api/options/<modelKey>`.
|
|
35
|
+
* This is the canonical path the kernel auto-derives from
|
|
36
|
+
* `ColumnDef.Ref`. Prefer this over `endpoint`.
|
|
37
|
+
*/
|
|
38
|
+
ref?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Free-text query forwarded as `?q=`. Empty values are skipped so the
|
|
41
|
+
* server returns the first page unfiltered.
|
|
42
|
+
*/
|
|
43
|
+
query?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Server-side pagination cap. Defaults to 50 (kernel
|
|
46
|
+
* DefaultOptionsLimit) if omitted.
|
|
47
|
+
*/
|
|
48
|
+
limit?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Toggle to disable fetching entirely (e.g. while a parent row is
|
|
51
|
+
* still loading). Defaults to true.
|
|
52
|
+
*/
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Escape hatch for callers that need a non-canonical URL — e.g.
|
|
56
|
+
* legacy `/options/<custom>?...`. When set it overrides `ref` and
|
|
57
|
+
* `modelKey` for the fetch path. The query string is built from
|
|
58
|
+
* `fieldKey` / `query` / `limit` exactly the same way.
|
|
59
|
+
*/
|
|
60
|
+
endpoint?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface UseOptionsResolverResult {
|
|
63
|
+
options: ResolvedOption[];
|
|
64
|
+
meta: OptionsMeta | null;
|
|
65
|
+
loading: boolean;
|
|
66
|
+
error: Error | null;
|
|
67
|
+
/** Forces a refetch. Useful after a parent record updates. */
|
|
68
|
+
refetch: () => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves select options for a field via the canonical
|
|
72
|
+
* `/api/options/:model?field=…` endpoint. Returns the v0.9.0 envelope
|
|
73
|
+
* `{ data, meta: { type, count } }` projected into a stable shape.
|
|
74
|
+
*
|
|
75
|
+
* The hook is intentionally minimal: it does NOT debounce `query`
|
|
76
|
+
* (callers should hold the controlled value and pass it post-debounce)
|
|
77
|
+
* and does NOT cache across hook instances (apps that need shared state
|
|
78
|
+
* compose this with TanStack Query in their own layer).
|
|
79
|
+
*/
|
|
80
|
+
export declare function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsResolverResult;
|
|
81
|
+
/**
|
|
82
|
+
* Normalizes the wire shape into ResolvedOption. The kernel returns dual
|
|
83
|
+
* id/value and label/name fields for legacy parity — we accept either
|
|
84
|
+
* and surface a stable shape downstream.
|
|
85
|
+
*/
|
|
86
|
+
export declare function projectOption(raw: any): ResolvedOption;
|
|
87
|
+
//# sourceMappingURL=use-options-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CAiHzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAatD"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// useOptionsResolver — single hook the SDK uses to fetch select options
|
|
2
|
+
// for a metadata-driven field. Replaces the ad-hoc `/data/<model>` reads
|
|
3
|
+
// that DynamicForm and DynamicRelation used to do.
|
|
4
|
+
//
|
|
5
|
+
// Contract (matches kernel ≥ v0.9.0):
|
|
6
|
+
// GET /api/options/:model?field=<key>&q=<text>&limit=<n>
|
|
7
|
+
// → { success: true, data: Option[], meta: { type: 'static'|'dynamic', count } }
|
|
8
|
+
//
|
|
9
|
+
// The hook prefers `ColumnDef.Ref` (auto-derived by the kernel from
|
|
10
|
+
// belongs_to relations) over a hand-wired `searchEndpoint`. Apps that
|
|
11
|
+
// adopt Ref via the kernel auto-derivation get the right behaviour for
|
|
12
|
+
// free; legacy callers that still ship `searchEndpoint` keep working.
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { useApi } from './api-context';
|
|
15
|
+
/**
|
|
16
|
+
* Resolves select options for a field via the canonical
|
|
17
|
+
* `/api/options/:model?field=…` endpoint. Returns the v0.9.0 envelope
|
|
18
|
+
* `{ data, meta: { type, count } }` projected into a stable shape.
|
|
19
|
+
*
|
|
20
|
+
* The hook is intentionally minimal: it does NOT debounce `query`
|
|
21
|
+
* (callers should hold the controlled value and pass it post-debounce)
|
|
22
|
+
* and does NOT cache across hook instances (apps that need shared state
|
|
23
|
+
* compose this with TanStack Query in their own layer).
|
|
24
|
+
*/
|
|
25
|
+
export function useOptionsResolver(args) {
|
|
26
|
+
const { modelKey, fieldKey, ref, query, limit, enabled = true, endpoint, } = args;
|
|
27
|
+
const api = useApi();
|
|
28
|
+
const [options, setOptions] = useState([]);
|
|
29
|
+
const [meta, setMeta] = useState(null);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
// refreshKey is bumped by `refetch` to force the effect to re-run
|
|
33
|
+
// even when none of the input args changed.
|
|
34
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
35
|
+
// The URL the hook hits. Ref wins over modelKey because the kernel's
|
|
36
|
+
// auto-derivation makes ref the canonical pointer; a manual endpoint
|
|
37
|
+
// wins over both as the explicit override.
|
|
38
|
+
const url = useMemo(() => {
|
|
39
|
+
if (endpoint)
|
|
40
|
+
return endpoint;
|
|
41
|
+
if (ref)
|
|
42
|
+
return `/options/${ref}`;
|
|
43
|
+
if (!modelKey)
|
|
44
|
+
return '';
|
|
45
|
+
return `/options/${modelKey}`;
|
|
46
|
+
}, [endpoint, ref, modelKey]);
|
|
47
|
+
// The field to query. When using `ref` the canonical lookup field is
|
|
48
|
+
// `id` (FK targets the target model's PK), unless the caller wants
|
|
49
|
+
// to override that explicitly via `fieldKey`. We only inject the `id`
|
|
50
|
+
// default when `ref` is set AND `fieldKey` is empty.
|
|
51
|
+
const effectiveField = useMemo(() => {
|
|
52
|
+
if (fieldKey)
|
|
53
|
+
return fieldKey;
|
|
54
|
+
if (ref)
|
|
55
|
+
return 'id';
|
|
56
|
+
return '';
|
|
57
|
+
}, [fieldKey, ref]);
|
|
58
|
+
// Track the in-flight controller so a new fetch can abort the
|
|
59
|
+
// previous one — matters for typeahead callers passing changing `query`.
|
|
60
|
+
const abortRef = useRef(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!enabled || !url || !effectiveField) {
|
|
63
|
+
setOptions([]);
|
|
64
|
+
setMeta(null);
|
|
65
|
+
setLoading(false);
|
|
66
|
+
setError(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Cancel any pending request before issuing a new one.
|
|
70
|
+
abortRef.current?.abort();
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
abortRef.current = controller;
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
const params = { field: effectiveField };
|
|
76
|
+
if (query)
|
|
77
|
+
params.q = query;
|
|
78
|
+
if (typeof limit === 'number' && limit > 0)
|
|
79
|
+
params.limit = limit;
|
|
80
|
+
api.get(url, { params, signal: controller.signal })
|
|
81
|
+
.then((res) => {
|
|
82
|
+
if (controller.signal.aborted)
|
|
83
|
+
return;
|
|
84
|
+
const body = res.data;
|
|
85
|
+
if (!body || body.success !== true) {
|
|
86
|
+
throw new Error(body?.message || 'options resolver: unsuccessful response');
|
|
87
|
+
}
|
|
88
|
+
const rawOptions = Array.isArray(body.data) ? body.data : [];
|
|
89
|
+
const projected = rawOptions.map(projectOption);
|
|
90
|
+
setOptions(projected);
|
|
91
|
+
// v0.9.0 envelope: meta.type / meta.count. We tolerate
|
|
92
|
+
// older deployments that still emit a root-level `type`
|
|
93
|
+
// by reading either spot — the projection prefers the
|
|
94
|
+
// canonical location so the SDK guides apps to the new
|
|
95
|
+
// shape without breaking grace-period upgrades.
|
|
96
|
+
const metaPayload = body.meta && typeof body.meta === 'object'
|
|
97
|
+
? body.meta
|
|
98
|
+
: { type: body.type, count: rawOptions.length };
|
|
99
|
+
setMeta({
|
|
100
|
+
type: metaPayload?.type ?? 'dynamic',
|
|
101
|
+
count: typeof metaPayload?.count === 'number'
|
|
102
|
+
? metaPayload.count
|
|
103
|
+
: rawOptions.length,
|
|
104
|
+
});
|
|
105
|
+
})
|
|
106
|
+
.catch((err) => {
|
|
107
|
+
if (controller.signal.aborted)
|
|
108
|
+
return;
|
|
109
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
110
|
+
setOptions([]);
|
|
111
|
+
setMeta(null);
|
|
112
|
+
})
|
|
113
|
+
.finally(() => {
|
|
114
|
+
if (!controller.signal.aborted)
|
|
115
|
+
setLoading(false);
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
controller.abort();
|
|
119
|
+
};
|
|
120
|
+
}, [api, url, effectiveField, query, limit, enabled, refreshKey]);
|
|
121
|
+
return {
|
|
122
|
+
options,
|
|
123
|
+
meta,
|
|
124
|
+
loading,
|
|
125
|
+
error,
|
|
126
|
+
refetch: () => setRefreshKey((k) => k + 1),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Normalizes the wire shape into ResolvedOption. The kernel returns dual
|
|
131
|
+
* id/value and label/name fields for legacy parity — we accept either
|
|
132
|
+
* and surface a stable shape downstream.
|
|
133
|
+
*/
|
|
134
|
+
export function projectOption(raw) {
|
|
135
|
+
const id = raw?.id ?? raw?.value ?? '';
|
|
136
|
+
const label = String(raw?.label ?? raw?.name ?? id ?? '');
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
value: raw?.value ?? id,
|
|
140
|
+
label,
|
|
141
|
+
name: String(raw?.name ?? label),
|
|
142
|
+
description: raw?.description ?? null,
|
|
143
|
+
image: raw?.image ?? null,
|
|
144
|
+
color: raw?.color ?? null,
|
|
145
|
+
icon: raw?.icon ?? null,
|
|
146
|
+
};
|
|
147
|
+
}
|