akanjs 3.0.0-alpha.35 → 3.0.0-alpha.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.35",
3
+ "version": "3.0.0-alpha.36",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -174,6 +174,18 @@ export class FormFields {
174
174
  return field.arrDepth > 0 || (field.modelRef as unknown) === Map || !!FormFields.#scalarModel(field);
175
175
  }
176
176
 
177
+ /**
178
+ * The embedded model an array field's rows are, or null for every other field.
179
+ *
180
+ * This is the one shape where writing the whole array is a hazard rather than an inconvenience: the agent has to
181
+ * echo every row it is *not* changing, `checked` validates types and not values, so one mistyped row it was never
182
+ * asked to touch is written silently. A relation array is excluded — its rows travel as ids, which are the payload
183
+ * and are refused by name when wrong — and so is an array of primitives, for the same reason.
184
+ */
185
+ static rowModelOf(field: ConstantField) {
186
+ return field.arrDepth > 0 ? FormFields.#scalarModel(field) : null;
187
+ }
188
+
177
189
  static #scalarModel(field: ConstantField) {
178
190
  const modelRef = field.modelRef as unknown as Cls;
179
191
  if (field.enum || PrimitiveRegistry.has(modelRef)) return null;
@@ -4,5 +4,6 @@ export * from "./readableValue";
4
4
  export * from "./StToolBuilder";
5
5
  export * from "./useFieldTool";
6
6
  export * from "./useFormTools";
7
+ export * from "./useRelationFieldTool";
7
8
  export * from "./useStExpose";
8
9
  export * from "./useStState";
@@ -1,9 +1,93 @@
1
1
  "use client";
2
+ import { capitalize } from "akanjs/common";
3
+ import type { JsonSchema, ToolEntry } from "../../vendor/use-agentic";
2
4
  import { useScopePath, useSurface } from "../../vendor/use-agentic";
3
5
  import { actionTagOf } from "../actionTag";
6
+ import { formSetterNames } from "../formSetterNames";
4
7
 
5
8
  import { useEffect, useRef } from "../hooks";
6
- import { FormFields } from "./formFields";
9
+ import { StoreRegistry } from "../storeRegistry";
10
+ import { type FormFieldRef, FormFields } from "./formFields";
11
+
12
+ /**
13
+ * A control's `transform` is what it does to every value a person types, so an agent's write goes through it too —
14
+ * otherwise a `Field.Phone` stores `010-1234-5678` for the person and the raw digits for the agent. It normalizes
15
+ * one scalar, so an array-valued control (`TextList`, `Tags`, `DoubleNumber`) applies it per element. A cleared
16
+ * nullable field stays null: a normalizer written for a value would turn it into one.
17
+ */
18
+ const normalized = (value: unknown, transform: unknown): unknown => {
19
+ if (typeof transform !== "function" || value === null) return value;
20
+ const apply = transform as (input: unknown) => unknown;
21
+ return Array.isArray(value) ? value.map((item) => apply(item)) : apply(value);
22
+ };
23
+
24
+ const dispatcherOf = (action: string) =>
25
+ StoreRegistry.instance.do[action] as ((...args: unknown[]) => unknown) | undefined;
26
+
27
+ const rowsOf = (ref: FormFieldRef): unknown[] => {
28
+ const form = StoreRegistry.instance.get()[`${ref.refName}Form`] as { [key: string]: unknown } | undefined;
29
+ const rows = form?.[ref.key];
30
+ return Array.isArray(rows) ? rows : [];
31
+ };
32
+
33
+ /**
34
+ * Append and remove-by-index for an array of embedded rows, beside the whole-array setter.
35
+ *
36
+ * Not new authority: the setter this control already published can produce any array these two can, so they are
37
+ * strictly weaker — which is what makes deriving them from the same field sound. What they add is that neither can
38
+ * touch a row it was not given, so the agent stops having to retype the rows it is leaving alone.
39
+ *
40
+ * Both take a list and act atomically. Removing indices one call at a time would shift the ones not yet removed, so
41
+ * `sub` filters the whole set at once, the way the generated action already does.
42
+ *
43
+ * `add` appends and publishes no insert position: the `+` a person presses always appends, and the framework cannot
44
+ * see the `limit` an app may pass from its own `onAdd`. `addOrSub` is never published — it matches by `indexOf`, so
45
+ * on rows it compares by reference and every toggle would append.
46
+ */
47
+ const rowEntries = (ref: FormFieldRef, arraySchema: JsonSchema): ToolEntry[] => {
48
+ if (!FormFields.rowModelOf(ref.field)) return [];
49
+ const names = formSetterNames(capitalize(ref.refName), ref.key);
50
+ if (!dispatcherOf(names.addFieldOnModel) || !dispatcherOf(names.subFieldOnModel)) return [];
51
+ return [
52
+ {
53
+ name: names.addFieldOnModel,
54
+ description: `Append rows to ${ref.key} on the ${ref.refName} form. Leaves every existing row untouched.`,
55
+ parameters: {
56
+ type: "object",
57
+ properties: { values: arraySchema },
58
+ required: ["values"],
59
+ additionalProperties: false,
60
+ },
61
+ effect: "state",
62
+ run: (args) => {
63
+ const checked = FormFields.checked(names.addFieldOnModel, "values", ref.field, args.values);
64
+ return dispatcherOf(names.addFieldOnModel)?.(checked);
65
+ },
66
+ },
67
+ {
68
+ name: names.subFieldOnModel,
69
+ description: `Remove rows of ${ref.key} from the ${ref.refName} form by their positions, counting from 0.`,
70
+ parameters: {
71
+ type: "object",
72
+ properties: { idxs: { type: "array", items: { type: "integer" } } },
73
+ required: ["idxs"],
74
+ additionalProperties: false,
75
+ },
76
+ effect: "state",
77
+ guard: (args) => {
78
+ const idxs = args.idxs;
79
+ if (!Array.isArray(idxs) || !idxs.length) return `"idxs" of ${names.subFieldOnModel} takes at least one index.`;
80
+ const length = rowsOf(ref).length;
81
+ const outside = idxs.filter(
82
+ (idx) => typeof idx !== "number" || !Number.isInteger(idx) || idx < 0 || idx >= length,
83
+ );
84
+ if (!outside.length) return true;
85
+ return `${ref.key} has ${length} ${length === 1 ? "row" : "rows"}, so ${outside.join(", ")} is out of range.`;
86
+ },
87
+ run: (args) => dispatcherOf(names.subFieldOnModel)?.(args.idxs),
88
+ },
89
+ ];
90
+ };
7
91
 
8
92
  /**
9
93
  * Publishes the setter a form control is already holding, for exactly as long as the control is on screen.
@@ -14,27 +98,34 @@ import { FormFields } from "./formFields";
14
98
  * consequences. Publishing from the form's subscription instead would offer every field of the model, including
15
99
  * the ones this template draws no control for.
16
100
  */
17
- export const useFieldTool = (onChange: unknown) => {
101
+ export const useFieldTool = (onChange: unknown, transform?: unknown) => {
18
102
  const surface = useSurface();
19
103
  const scope = useScopePath();
20
104
  const action = actionTagOf(onChange)?.action ?? null;
21
- const live = useRef(onChange);
22
- live.current = onChange;
105
+ const live = useRef({ onChange, transform });
106
+ live.current = { onChange, transform };
23
107
  const scopeKey = scope.join(".");
24
108
  useEffect(() => {
25
109
  if (!action) return;
26
110
  const ref = FormFields.ref(action);
27
111
  const schema = ref && FormFields.schema(ref.field);
28
112
  if (!ref || !schema) return;
29
- return surface.registerTool(scope, {
30
- name: action,
31
- description: `Set ${ref.key} on the ${ref.refName} form.`,
32
- parameters: { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false },
33
- effect: "state",
34
- run: (args) =>
35
- (live.current as (value: unknown) => unknown)(
36
- FormFields.checked(action, "value", ref.field, args.value === undefined ? null : args.value),
37
- ),
38
- });
113
+ const entries: ToolEntry[] = [
114
+ {
115
+ name: action,
116
+ description: `Set ${ref.key} on the ${ref.refName} form.`,
117
+ parameters: { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false },
118
+ effect: "state",
119
+ run: (args) => {
120
+ const checked = FormFields.checked(action, "value", ref.field, args.value === undefined ? null : args.value);
121
+ return (live.current.onChange as (value: unknown) => unknown)(normalized(checked, live.current.transform));
122
+ },
123
+ },
124
+ ...rowEntries(ref, schema),
125
+ ];
126
+ const registered = entries.map((entry) => surface.registerTool(scope, entry));
127
+ return () => {
128
+ for (const unregister of registered) unregister();
129
+ };
39
130
  }, [surface, scopeKey, action]);
40
131
  };
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { type Cls, type DataList, PrimitiveRegistry } from "akanjs/base";
3
+ import { capitalize } from "akanjs/common";
4
+ import { type ConstantField, ConstantRegistry } from "akanjs/constant";
5
+ import type { JsonSchema } from "../../vendor/use-agentic";
6
+ import { useScopePath, useSurface } from "../../vendor/use-agentic";
7
+ import { actionTagOf } from "../actionTag";
8
+
9
+ import { useEffect, useRef } from "../hooks";
10
+ import { FormFields } from "./formFields";
11
+
12
+ export interface RelationFieldSource<T extends { id: string }> {
13
+ /** Read live from the store rather than closed over: `load` below changes the list mid-call. */
14
+ read: () => DataList<T>;
15
+ /** Loads the options an agent never opened the dropdown to fetch. */
16
+ load: () => Promise<unknown> | unknown;
17
+ /** How one option reads to a person, so the agent can match what it sees on screen. */
18
+ label: (model: T) => string;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ /** The database model a relation field points at, or null for anything else — a primitive, an enum, a scalar. */
23
+ const relationOf = (field: ConstantField): string | null => {
24
+ const modelRef = field.modelRef as unknown as Cls;
25
+ if (field.fieldType !== "property" || field.enum || !modelRef || PrimitiveRegistry.has(modelRef)) return null;
26
+ const refName = ConstantRegistry.getRefName(modelRef, { allowEmpty: true });
27
+ return refName && ConstantRegistry.database.has(refName) ? refName : null;
28
+ };
29
+
30
+ /**
31
+ * The two tools a relation picker owes an agent: list the documents it can pick, then pick by id.
32
+ *
33
+ * `FormFields` publishes no schema for a relation, and it is right not to — the form holds the whole related
34
+ * document, so an id would need a lookup the store does not do. The picker is where that lookup lives: it holds
35
+ * the slice list, the loader, and the label each option renders with. So the field reaches an agent from the one
36
+ * component that can resolve it, which is the rule every other control follows — the control is the declaration.
37
+ *
38
+ * Listing is its own tool because loading is its own step for a person too: the options arrive when the dropdown
39
+ * opens, and an agent never opens it. Folding the load into the setter would leave an agent guessing ids in order
40
+ * to learn them from the refusal.
41
+ */
42
+ export const useRelationFieldTool = <T extends { id: string }>(
43
+ onChange: unknown,
44
+ { read, load, label, disabled }: RelationFieldSource<T>,
45
+ ) => {
46
+ const surface = useSurface();
47
+ const scope = useScopePath();
48
+ const action = actionTagOf(onChange)?.action ?? null;
49
+ const live = useRef({ onChange, read, load, label });
50
+ live.current = { onChange, read, load, label };
51
+ const scopeKey = scope.join(".");
52
+ useEffect(() => {
53
+ if (!action || disabled) return;
54
+ const ref = FormFields.ref(action);
55
+ const target = ref && relationOf(ref.field);
56
+
57
+ if (!ref || !target || FormFields.schema(ref.field)) return;
58
+ const many = ref.field.arrDepth > 0;
59
+ const nullable = !!ref.field.nullable && !many;
60
+ const listName = `load${capitalize(ref.key)}OptionsOn${capitalize(ref.refName)}`;
61
+ const argName = many ? `${ref.key}Ids` : `${ref.key}Id`;
62
+ const id: JsonSchema = { type: "string" };
63
+ const options = () => live.current.read().map((model) => ({ id: model.id, label: live.current.label(model) }));
64
+ const idsIn = (args: Record<string, unknown>): unknown => {
65
+ const value = args[argName];
66
+ if (nullable && (value === null || value === undefined)) return [];
67
+ return many ? value : [value];
68
+ };
69
+ const offList = surface.registerTool(scope, {
70
+ name: listName,
71
+ description: `List the ${target}s the ${ref.refName} form can pick for ${ref.key}, loading them first. Pass an id from it to ${action}.`,
72
+ effect: "query",
73
+ run: async () => {
74
+ await live.current.load();
75
+ return options();
76
+ },
77
+ });
78
+ const offSet = surface.registerTool(scope, {
79
+ name: action,
80
+ description: `Set ${ref.key} on the ${ref.refName} form to ${many ? `${target}s` : `one ${target}`}, by id. Call ${listName} first for the ids.`,
81
+ parameters: {
82
+ type: "object",
83
+ properties: { [argName]: many ? { type: "array", items: id } : id },
84
+ ...(nullable ? {} : { required: [argName] }),
85
+ additionalProperties: false,
86
+ },
87
+ effect: "state",
88
+ guard: (args) => {
89
+ const ids = idsIn(args);
90
+ if (!Array.isArray(ids)) return `"${argName}" of ${action} must be an array of ids.`;
91
+ const list = live.current.read();
92
+ const missing = ids.filter((value) => typeof value !== "string" || !list.get(value));
93
+ if (!missing.length) return true;
94
+ if (!list.length) return `No ${target} is loaded yet. Call ${listName} first for the ids.`;
95
+ return `The ${ref.refName} form offers no ${target} ${missing.join(", ")}. It offers: ${list
96
+ .map((model) => `${model.id} (${live.current.label(model)})`)
97
+ .join(", ")}.`;
98
+ },
99
+ run: (args) => {
100
+ const list = live.current.read();
101
+ const setter = live.current.onChange as (value: unknown) => unknown;
102
+ const picked = (idsIn(args) as string[]).flatMap((value) => {
103
+ const model = list.get(value);
104
+ return model ? [model] : [];
105
+ });
106
+ return setter(many ? picked : (picked[0] ?? null));
107
+ },
108
+ });
109
+ return () => {
110
+ offList();
111
+ offSet();
112
+ };
113
+ }, [surface, scopeKey, action, disabled]);
114
+ };
@@ -41,4 +41,13 @@ export declare class FormFields {
41
41
  static checked(action: string, path: string, field: ConstantField, value: unknown, depth?: number): unknown;
42
42
  /** A field no annotated control can ever name: its rows are written through `writeOn<Model>(path, value)`. */
43
43
  static isComposite(field: ConstantField): boolean;
44
+ /**
45
+ * The embedded model an array field's rows are, or null for every other field.
46
+ *
47
+ * This is the one shape where writing the whole array is a hazard rather than an inconvenience: the agent has to
48
+ * echo every row it is *not* changing, `checked` validates types and not values, so one mistyped row it was never
49
+ * asked to touch is written silently. A relation array is excluded — its rows travel as ids, which are the payload
50
+ * and are refused by name when wrong — and so is an array of primitives, for the same reason.
51
+ */
52
+ static rowModelOf(field: ConstantField): (new (...args: never[]) => unknown) | null;
44
53
  }
@@ -4,5 +4,6 @@ export * from "./readableValue.d.ts";
4
4
  export * from "./StToolBuilder.d.ts";
5
5
  export * from "./useFieldTool.d.ts";
6
6
  export * from "./useFormTools.d.ts";
7
+ export * from "./useRelationFieldTool.d.ts";
7
8
  export * from "./useStExpose.d.ts";
8
9
  export * from "./useStState.d.ts";
@@ -7,4 +7,4 @@
7
7
  * consequences. Publishing from the form's subscription instead would offer every field of the model, including
8
8
  * the ones this template draws no control for.
9
9
  */
10
- export declare const useFieldTool: (onChange: unknown) => void;
10
+ export declare const useFieldTool: (onChange: unknown, transform?: unknown) => void;
@@ -0,0 +1,27 @@
1
+ import { type DataList } from "akanjs/base";
2
+ export interface RelationFieldSource<T extends {
3
+ id: string;
4
+ }> {
5
+ /** Read live from the store rather than closed over: `load` below changes the list mid-call. */
6
+ read: () => DataList<T>;
7
+ /** Loads the options an agent never opened the dropdown to fetch. */
8
+ load: () => Promise<unknown> | unknown;
9
+ /** How one option reads to a person, so the agent can match what it sees on screen. */
10
+ label: (model: T) => string;
11
+ disabled?: boolean;
12
+ }
13
+ /**
14
+ * The two tools a relation picker owes an agent: list the documents it can pick, then pick by id.
15
+ *
16
+ * `FormFields` publishes no schema for a relation, and it is right not to — the form holds the whole related
17
+ * document, so an id would need a lookup the store does not do. The picker is where that lookup lives: it holds
18
+ * the slice list, the loader, and the label each option renders with. So the field reaches an agent from the one
19
+ * component that can resolve it, which is the rule every other control follows — the control is the declaration.
20
+ *
21
+ * Listing is its own tool because loading is its own step for a person too: the options arrive when the dropdown
22
+ * opens, and an agent never opens it. Folding the load into the setter would leave an agent guessing ids in order
23
+ * to learn them from the refusal.
24
+ */
25
+ export declare const useRelationFieldTool: <T extends {
26
+ id: string;
27
+ }>(onChange: unknown, { read, load, label, disabled }: RelationFieldSource<T>) => void;
package/ui/Field.tsx CHANGED
@@ -3,7 +3,7 @@ import { type DataList, type Dayjs, dayjs, type EnumInstance, isEnum } from "aka
3
3
  import { cn, usePage } from "akanjs/client";
4
4
  import { capitalize, formatPhone, isPhoneNumber, lowerlize } from "akanjs/common";
5
5
  import type { SliceMeta } from "akanjs/fetch";
6
- import { st, useFieldTool } from "akanjs/store";
6
+ import { st, useFieldTool, useRelationFieldTool } from "akanjs/store";
7
7
  import { memo, type ReactNode, useState } from "react";
8
8
  import { AiOutlinePlus } from "react-icons/ai";
9
9
  import { BiHelpCircle, BiTrash, BiX } from "react-icons/bi";
@@ -104,8 +104,9 @@ const List = <Item,>({
104
104
  }: ListProps<Item>) => {
105
105
  const { l } = usePage();
106
106
  const recipe = useUiRecipe("button") ?? buttonRecipe;
107
+ useFieldTool(onChange);
107
108
  return (
108
- <div className={cn("flex w-full flex-col", className)}>
109
+ <div {...agentAttrs(onChange)} className={cn("flex w-full flex-col", className)}>
109
110
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
110
111
  <div className="mb-2 flex w-full flex-col gap-2 rounded-box border border-border p-2">
111
112
  {value.map((item, idx) => (
@@ -181,7 +182,7 @@ const Text = ({
181
182
  inputClassName,
182
183
  inputStyleType = "bordered",
183
184
  }: TextProps) => {
184
- useFieldTool(onChange);
185
+ useFieldTool(onChange, transform);
185
186
  const { l } = usePage();
186
187
  return (
187
188
  <div className={cn("flex flex-col", className)}>
@@ -247,7 +248,7 @@ const Price = ({
247
248
  inputClassName,
248
249
  inputStyleType = "bordered",
249
250
  }: PriceProps) => {
250
- useFieldTool(onChange);
251
+ useFieldTool(onChange, transform);
251
252
  const { l } = usePage();
252
253
  return (
253
254
  <div className={cn("flex flex-col", className)}>
@@ -315,7 +316,7 @@ const TextArea = ({
315
316
  cache,
316
317
  inputClassName,
317
318
  }: TextAreaProps) => {
318
- useFieldTool(onChange);
319
+ useFieldTool(onChange, transform);
319
320
  const { l } = usePage();
320
321
  return (
321
322
  <div className={cn("flex flex-col", className)}>
@@ -544,7 +545,7 @@ const TextList = ({
544
545
  validate,
545
546
  inputClassName,
546
547
  }: TextListProps) => {
547
- useFieldTool(onChange);
548
+ useFieldTool(onChange, transform);
548
549
  const { l } = usePage();
549
550
  const recipe = useUiRecipe("button") ?? buttonRecipe;
550
551
  return (
@@ -651,7 +652,7 @@ const Tags = ({
651
652
  validate,
652
653
  inputClassName,
653
654
  }: TagsProps) => {
654
- useFieldTool(onChange);
655
+ useFieldTool(onChange, transform);
655
656
  const { l } = usePage();
656
657
  const badge = useUiRecipe("badge") ?? badgeRecipe;
657
658
  const [inputVisible, setInputVisible] = useState(false);
@@ -814,9 +815,7 @@ const DateRange = <Nullable extends boolean>({
814
815
  value={from}
815
816
  max={max}
816
817
  min={min}
817
- onChange={(value: Dayjs) => {
818
- onChangeFrom(value);
819
- }}
818
+ onChange={onChangeFrom}
820
819
  />
821
820
  </div>
822
821
  <div className="relative flex w-full flex-col items-start gap-2 text-center md:flex-row md:items-center">
@@ -828,9 +827,7 @@ const DateRange = <Nullable extends boolean>({
828
827
  value={to}
829
828
  max={max}
830
829
  min={min}
831
- onChange={(value: Dayjs) => {
832
- onChangeTo(value);
833
- }}
830
+ onChange={onChangeTo}
834
831
  />
835
832
  </div>
836
833
  </div>
@@ -881,7 +878,7 @@ const Number = ({
881
878
  formatter,
882
879
  parser,
883
880
  }: NumberProps) => {
884
- useFieldTool(onChange);
881
+ useFieldTool(onChange, transform);
885
882
  const { l } = usePage();
886
883
  return (
887
884
  <div className={cn("flex flex-col", className)}>
@@ -953,7 +950,7 @@ const DoubleNumber = ({
953
950
  validate,
954
951
  onPressEnter,
955
952
  }: DoubleNumberProps) => {
956
- useFieldTool(onChange);
953
+ useFieldTool(onChange, transform);
957
954
  const { l } = usePage();
958
955
  return (
959
956
  <div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
@@ -1041,7 +1038,7 @@ const Email = ({
1041
1038
  inputClassName,
1042
1039
  inputStyleType,
1043
1040
  }: EmailProps) => {
1044
- useFieldTool(onChange);
1041
+ useFieldTool(onChange, transform);
1045
1042
  const { l } = usePage();
1046
1043
  return (
1047
1044
  <div className={cn("flex flex-col", className)}>
@@ -1106,7 +1103,7 @@ const Phone = ({
1106
1103
  onPressEnter,
1107
1104
  inputClassName,
1108
1105
  }: PhoneProps) => {
1109
- useFieldTool(onChange);
1106
+ useFieldTool(onChange, transform);
1110
1107
  const { l } = usePage();
1111
1108
 
1112
1109
  return (
@@ -1178,7 +1175,7 @@ const Password = ({
1178
1175
  inputClassName,
1179
1176
  showConfirm,
1180
1177
  }: PasswordProps) => {
1181
- useFieldTool(onChange);
1178
+ useFieldTool(onChange, transform);
1182
1179
  const { l } = usePage();
1183
1180
  return (
1184
1181
  <div className={cn("flex flex-col", className)}>
@@ -1242,6 +1239,12 @@ interface ParentProps<T extends string, State, Input, Full, Light> {
1242
1239
  renderOption: (model: Light) => ReactNode;
1243
1240
  renderSelected?: (value: Light) => ReactNode;
1244
1241
  }
1242
+ /** The one line an option renders as, so an agent can match an id against what it reads on screen. */
1243
+ const optionLabel = <Light extends { id: string }>(model: Light, render: (model: Light) => ReactNode) => {
1244
+ const rendered = render(model);
1245
+ return typeof rendered === "string" ? rendered : model.id;
1246
+ };
1247
+
1245
1248
  const Parent = <T extends string, State, Input, Full extends { id: string }, Light extends { id: string }>({
1246
1249
  label,
1247
1250
  desc,
@@ -1263,6 +1266,7 @@ const Parent = <T extends string, State, Input, Full extends { id: string }, Lig
1263
1266
  const [modelName, ModelName] = [lowerlize(refName), capitalize(refName)];
1264
1267
  const storeUse = st.use as { [key: string]: () => unknown };
1265
1268
  const storeDo = st.do as unknown as { [key: string]: (...args: any[]) => Promise<void> };
1269
+ const storeGet = st.get as unknown as <V>() => { [key: string]: V };
1266
1270
 
1267
1271
  const names = {
1268
1272
  model: modelName,
@@ -1279,9 +1283,15 @@ const Parent = <T extends string, State, Input, Full extends { id: string }, Lig
1279
1283
 
1280
1284
  const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
1281
1285
  const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
1286
+ useRelationFieldTool(onChange, {
1287
+ read: () => storeGet<DataList<Light>>()[namesOfSlice.modelList],
1288
+ load: () => storeDo[namesOfSlice.refreshModel]({ invalidate: true, queryArgs: initArgs }),
1289
+ label: (model) => optionLabel(model, renderOption),
1290
+ disabled,
1291
+ });
1282
1292
 
1283
1293
  return (
1284
- <div className={cn("flex flex-col", className)}>
1294
+ <div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
1285
1295
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
1286
1296
  <Select<string | null, false, true>
1287
1297
  label={label}
@@ -1373,9 +1383,11 @@ const ParentId = <T extends string, State, Input, Full extends { id: string }, L
1373
1383
  };
1374
1384
  const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
1375
1385
  const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
1386
+
1387
+ useFieldTool(onChange);
1376
1388
 
1377
1389
  return (
1378
- <div className={cn("flex flex-col", className)}>
1390
+ <div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
1379
1391
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
1380
1392
  <Select<string | null, false, true>
1381
1393
  searchable
@@ -1464,9 +1476,15 @@ const Children = <T extends string, State, Input, Full extends { id: string }, L
1464
1476
  };
1465
1477
  const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
1466
1478
  const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
1479
+ useRelationFieldTool(onChange, {
1480
+ read: () => storeGet<DataList<Light>>()[namesOfSlice.modelList],
1481
+ load: () => storeDo[namesOfSlice.refreshModel]({ invalidate: true, queryArgs: initArgs }),
1482
+ label: (model) => optionLabel(model, renderOption),
1483
+ disabled,
1484
+ });
1467
1485
 
1468
1486
  return (
1469
- <div className={cn("flex flex-col", className)}>
1487
+ <div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
1470
1488
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
1471
1489
  <Select
1472
1490
  searchable
@@ -1554,9 +1572,11 @@ const ChildrenId = <T extends string, State, Input, Full extends { id: string },
1554
1572
  };
1555
1573
  const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
1556
1574
  const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
1575
+
1576
+ useFieldTool(onChange);
1557
1577
 
1558
1578
  return (
1559
- <div className={cn("flex flex-col", className)}>
1579
+ <div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
1560
1580
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
1561
1581
  <Select
1562
1582
  searchable