@adcops/autocore-react 3.3.113 → 3.3.116
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/dist/components/index.d.ts +10 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/CycleConfigDialog.d.ts +39 -0
- package/dist/components/tis/CycleConfigDialog.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigDialog.js +1 -0
- package/dist/components/tis/CycleConfigForm.d.ts +22 -0
- package/dist/components/tis/CycleConfigForm.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigForm.js +1 -0
- package/dist/components/tis/CycleStatusStrip.d.ts +10 -0
- package/dist/components/tis/CycleStatusStrip.d.ts.map +1 -0
- package/dist/components/tis/CycleStatusStrip.js +1 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/ScienceTable.d.ts +15 -0
- package/dist/components/tis/ScienceTable.d.ts.map +1 -1
- package/dist/components/tis/ScienceTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +14 -4
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestFieldRow.d.ts +151 -0
- package/dist/components/tis/TestFieldRow.d.ts.map +1 -0
- package/dist/components/tis/TestFieldRow.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +15 -51
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TestSetupView.d.ts.map +1 -1
- package/dist/components/tis/TestSetupView.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +80 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts +19 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.js +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +3 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +28 -1
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/package.json +110 -110
- package/src/components/index.ts +14 -0
- package/src/components/tis/CycleConfigDialog.tsx +173 -0
- package/src/components/tis/CycleConfigForm.tsx +192 -0
- package/src/components/tis/CycleStatusStrip.tsx +84 -0
- package/src/components/tis/ResultHistoryTable.tsx +141 -1
- package/src/components/tis/ScienceTable.tsx +71 -12
- package/src/components/tis/TestDataView.tsx +82 -7
- package/src/components/tis/TestFieldRow.tsx +297 -0
- package/src/components/tis/TestSetupForm.tsx +52 -212
- package/src/components/tis/TestSetupView.tsx +60 -3
- package/src/components/tis/TisProvider.tsx +191 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +1 -0
- package/src/components/tis-editor/editor/CycleConfigSection.tsx +93 -0
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +10 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +10 -4
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +22 -2
- package/src/components/tis-editor/editor/ViewsEditor.tsx +3 -0
- package/src/components/tis-editor/types.ts +34 -1
- package/src/components/tis-editor/validation.ts +20 -2
|
@@ -72,6 +72,12 @@ export interface TestFieldDef {
|
|
|
72
72
|
* (label === stored value) or an explicit { label, value } pair.
|
|
73
73
|
* Mirrors the Rust TestField::options (untagged FieldOption). */
|
|
74
74
|
options?: Array<string | number | boolean | { label: string; value: any }>;
|
|
75
|
+
/** Operator-editable after recording — annotation columns only (a specimen
|
|
76
|
+
* or lot id noted against a captured row). Meaningful on `cycle_fields`;
|
|
77
|
+
* the server refuses `tis.update_cycle` for any field without it, so
|
|
78
|
+
* measured columns can never be typed over. Mirrors Rust
|
|
79
|
+
* `TestField::editable`. */
|
|
80
|
+
editable?: boolean;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
export interface ChartAxis { field?: string; column?: string; label?: string; }
|
|
@@ -155,10 +161,20 @@ export interface TestConfiguration {
|
|
|
155
161
|
defaults?: { [field: string]: any };
|
|
156
162
|
}
|
|
157
163
|
export interface TestMethod {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
/* All four field lists are OPTIONAL by design. Server-side each is
|
|
165
|
+
* `#[serde(default)]`, so a method that declares only `config_fields` is
|
|
166
|
+
* perfectly valid and the other keys simply aren't in the JSON. Typing
|
|
167
|
+
* them as required let `schema.cycle_fields.map(...)` compile and then
|
|
168
|
+
* blank the operator's page at runtime (gen4_ac, 2026-08-11). Keep them
|
|
169
|
+
* optional so the compiler demands a `?? []` at every use. */
|
|
170
|
+
project_fields?: TestFieldDef[];
|
|
171
|
+
config_fields?: TestFieldDef[];
|
|
172
|
+
/** Operator-entered per-row annotation. Recorded onto each cycle row
|
|
173
|
+
* by the server and rendered as the first Cycle Data columns after
|
|
174
|
+
* the row index, in declared order. */
|
|
175
|
+
cycle_config_fields?: TestFieldDef[];
|
|
176
|
+
cycle_fields?: TestFieldDef[];
|
|
177
|
+
results_fields?: TestFieldDef[];
|
|
162
178
|
raw_data?: RawDataShape | null;
|
|
163
179
|
views?: { [name: string]: ChartView };
|
|
164
180
|
/** Optional pretty label for the Test Method picker. */
|
|
@@ -208,6 +224,46 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
208
224
|
const [rawOpen, setRawOpen] = useState(false);
|
|
209
225
|
const [configOpen, setConfigOpen] = useState(false);
|
|
210
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Commit an operator edit to an annotation cell (`tis.update_cycle`).
|
|
229
|
+
*
|
|
230
|
+
* The server is the gate: it accepts a field only if the method declares it
|
|
231
|
+
* `editable: true`, so a measured column is refused even if a caller asks.
|
|
232
|
+
* We update local state only after the round trip succeeds — showing an edit
|
|
233
|
+
* that didn't persist is worse than showing the old value.
|
|
234
|
+
*/
|
|
235
|
+
const saveCycleField = useCallback(
|
|
236
|
+
async (row: any, field: string, value: any) => {
|
|
237
|
+
const cycleIndex = row?.cycle_index;
|
|
238
|
+
if (!projectId || !methodId || !runId || !cycleIndex) return;
|
|
239
|
+
try {
|
|
240
|
+
const resp: any = await invoke(
|
|
241
|
+
'tis.update_cycle' as any,
|
|
242
|
+
MessageType.Request,
|
|
243
|
+
{
|
|
244
|
+
project_id: projectId,
|
|
245
|
+
method_id: methodId,
|
|
246
|
+
run_id: runId,
|
|
247
|
+
cycle_index: cycleIndex,
|
|
248
|
+
fields: { [field]: value },
|
|
249
|
+
} as any,
|
|
250
|
+
);
|
|
251
|
+
if (resp?.success === false) {
|
|
252
|
+
console.error('tis.update_cycle rejected:', resp?.error_message);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
setCycles((prev) =>
|
|
256
|
+
prev.map((c) =>
|
|
257
|
+
c?.cycle_index === cycleIndex ? { ...c, [field]: value } : c,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error('tis.update_cycle failed:', e);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
[invoke, projectId, methodId, runId],
|
|
265
|
+
);
|
|
266
|
+
|
|
211
267
|
// Direct handle on the chart.js instance so the toolbar's reset-
|
|
212
268
|
// zoom button can call chart.resetZoom() — the zoom plugin's only
|
|
213
269
|
// imperative API. Customers like the wheel/pinch/drag zoom but
|
|
@@ -742,12 +798,30 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
742
798
|
doesn't render 1000 row elements at once. */}
|
|
743
799
|
{(() => {
|
|
744
800
|
const useVirtual = cycles.length > CYCLE_VIRTUAL_THRESHOLD;
|
|
745
|
-
|
|
801
|
+
// `?? []` is load-bearing: cycle_fields is `#[serde(default)]`
|
|
802
|
+
// server-side, so a method that declares only config_fields is
|
|
803
|
+
// valid and arrives with the key absent. Without the guard the
|
|
804
|
+
// `.map` throws inside render and React unmounts the whole
|
|
805
|
+
// app — the operator sees a blank page, not a missing table.
|
|
806
|
+
// Operator-entered annotation leads, right after the row
|
|
807
|
+
// index, then the measured columns — the same order the
|
|
808
|
+
// server writes into every CSV export. Annotation columns
|
|
809
|
+
// are always editable: they're the operator's own note, so
|
|
810
|
+
// correcting a mistyped one is the intended workflow (the
|
|
811
|
+
// server accepts cycle_config_fields on tis.update_cycle
|
|
812
|
+
// without an `editable` flag), while measured columns still
|
|
813
|
+
// have to opt in.
|
|
814
|
+
const toColumn = (f: TestFieldDef, editable: boolean): ScienceColumn => ({
|
|
746
815
|
field: f.name,
|
|
747
816
|
label: f.name,
|
|
748
817
|
units: f.units,
|
|
818
|
+
editable,
|
|
749
819
|
body: (row) => formatCell(row[f.name], f.type, f.scale),
|
|
750
|
-
})
|
|
820
|
+
});
|
|
821
|
+
const cycleColumns: ScienceColumn[] = [
|
|
822
|
+
...(schema.cycle_config_fields ?? []).map(f => toColumn(f, true)),
|
|
823
|
+
...(schema.cycle_fields ?? []).map(f => toColumn(f, !!f.editable)),
|
|
824
|
+
];
|
|
751
825
|
return (
|
|
752
826
|
<ScienceTable
|
|
753
827
|
title={`Cycle Data (${cycles.length})`}
|
|
@@ -756,6 +830,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
756
830
|
scrollable={useVirtual}
|
|
757
831
|
scrollHeight={useVirtual ? cycleTableHeight : undefined}
|
|
758
832
|
emptyMessage="No cycles yet."
|
|
833
|
+
onCellEdit={saveCycleField}
|
|
759
834
|
/>
|
|
760
835
|
);
|
|
761
836
|
})()}
|
|
@@ -767,7 +842,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
767
842
|
{(() => {
|
|
768
843
|
const values = { ...results, ...sampleSummary };
|
|
769
844
|
const hasValues = Object.keys(values).length > 0;
|
|
770
|
-
const resultColumns: ScienceColumn[] = schema.results_fields.map(f => ({
|
|
845
|
+
const resultColumns: ScienceColumn[] = (schema.results_fields ?? []).map(f => ({
|
|
771
846
|
field: f.name,
|
|
772
847
|
label: f.name,
|
|
773
848
|
units: f.units,
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* The one place a TIS `TestField` becomes an input.
|
|
5
|
+
*
|
|
6
|
+
* Test Configuration (<TestSetupForm>, per-test) and Cycle Configuration
|
|
7
|
+
* (<CycleConfigForm>, per-row) render the same declarative field schema
|
|
8
|
+
* with the same display/raw unit convention, the same emptiness and range
|
|
9
|
+
* rules, and the same 4-column `ac-form-grid` row shape. They differ only
|
|
10
|
+
* in where the value is stored and when it is committed, so the rendering
|
|
11
|
+
* and the validation rules live here and both forms call in.
|
|
12
|
+
*
|
|
13
|
+
* Storage is always RAW; `scale` converts only at the boundary:
|
|
14
|
+
* display = raw * scale raw = display / scale
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { Button } from 'primereact/button';
|
|
19
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
20
|
+
import { ValueInput } from '../ValueInput';
|
|
21
|
+
import { TextInput } from '../TextInput';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One field in a test method's schema. Mirrors the Rust `TestField`
|
|
25
|
+
* (autocore-ams `config.rs`) — the same shape backs project_fields,
|
|
26
|
+
* config_fields, cycle_config_fields, cycle_fields and results_fields.
|
|
27
|
+
*/
|
|
28
|
+
export interface TestFieldDef {
|
|
29
|
+
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
30
|
+
name: string;
|
|
31
|
+
type: string;
|
|
32
|
+
units?: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
source?: string;
|
|
35
|
+
/** Pretty label rendered by the form. Falls back to `name`. Units
|
|
36
|
+
* are appended automatically; don't pre-format `[mm]` into label. */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** Seed value applied when the operator selects this test method.
|
|
41
|
+
* For source-bound fields the default is written to the GM tag so
|
|
42
|
+
* the control program sees it; for non-source fields it's stashed
|
|
43
|
+
* straight into stagedConfig. Operator edits override per-stage,
|
|
44
|
+
* but the schema default never mutates — re-selecting the method
|
|
45
|
+
* re-applies the default. **Authored in display units** when
|
|
46
|
+
* `scale` is set — the form divides by `scale` before writing to
|
|
47
|
+
* GM / stagedConfig so the underlying storage stays raw. */
|
|
48
|
+
default?: any;
|
|
49
|
+
/** Optional unit-conversion multiplier. Mirrors AutoCoreTagContext:
|
|
50
|
+
* ```
|
|
51
|
+
* display = raw * scale
|
|
52
|
+
* raw = display / scale
|
|
53
|
+
* ```
|
|
54
|
+
* Example: backend stores degrees, operator enters revolutions →
|
|
55
|
+
* `scale: 0.00277778`. None / 1.0 = no conversion.
|
|
56
|
+
*
|
|
57
|
+
* Storage stays raw — the form scales only on input and display.
|
|
58
|
+
* Cycle and results values are scaled by the corresponding paths
|
|
59
|
+
* in TestDataView; the server scales CSV exports too. */
|
|
60
|
+
scale?: number;
|
|
61
|
+
/** Optional inclusive bounds (display units, same convention as `default`
|
|
62
|
+
* and `scale`) for the operator's numeric entry. The numeric input
|
|
63
|
+
* rejects values outside `[min, max]`; non-numeric fields ignore them. */
|
|
64
|
+
min?: number;
|
|
65
|
+
max?: number;
|
|
66
|
+
/** Optional fixed set of choices. When present, the field renders
|
|
67
|
+
* as a dropdown and the operator must pick one of the declared
|
|
68
|
+
* values rather than typing freely. Each entry is either a bare
|
|
69
|
+
* scalar (label === value) or an explicit `{ label, value }` pair
|
|
70
|
+
* when the displayed text should differ from the stored value.
|
|
71
|
+
* Works with any `type`; like `default`, values are authored in
|
|
72
|
+
* display units when `scale` is set. */
|
|
73
|
+
options?: Array<
|
|
74
|
+
| string
|
|
75
|
+
| number
|
|
76
|
+
| boolean
|
|
77
|
+
| { label?: string; value: string | number | boolean }
|
|
78
|
+
>;
|
|
79
|
+
/** Operator-editable after recording. Meaningful on `cycle_fields`;
|
|
80
|
+
* `cycle_config_fields` are always editable (they're annotation, not
|
|
81
|
+
* measurement) and don't need the flag. */
|
|
82
|
+
editable?: boolean;
|
|
83
|
+
/** `cycle_config_fields` only: clear this entry once a row has recorded
|
|
84
|
+
* it, so the next row can't silently inherit a stale annotation. Fires
|
|
85
|
+
* at the method's `cycle_config_scope` boundary. */
|
|
86
|
+
autoclear?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Which boundary an operator annotates `cycle_config_fields` at. Mirrors
|
|
91
|
+
* the Rust `CycleConfigScope`.
|
|
92
|
+
*
|
|
93
|
+
* `'run'` one latch per Start — the same values stamp every cycle of
|
|
94
|
+
* the run and clear when it closes. Default, and the only
|
|
95
|
+
* correct choice for a machine that records many cycles from
|
|
96
|
+
* one Start.
|
|
97
|
+
* `'cycle'` one latch per cycle — values clear after each recorded row
|
|
98
|
+
* and the gate re-arms, so the operator annotates every one.
|
|
99
|
+
*/
|
|
100
|
+
export type CycleConfigScope = 'run' | 'cycle';
|
|
101
|
+
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
// Display ↔ raw boundary
|
|
104
|
+
// -------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convention: `display = raw * scale`, `raw = display / scale`. Matches
|
|
108
|
+
* AutoCoreTagContext. Default scale = 1.0 (no conversion). Non-numeric
|
|
109
|
+
* values (string, null, undefined, NaN) pass through unchanged — the
|
|
110
|
+
* forms share one change handler across fields of every type.
|
|
111
|
+
*
|
|
112
|
+
* Storage (config map, GM, on-disk JSON) always holds the raw value. Only
|
|
113
|
+
* the inputs the operator looks at and the cells in TestDataView use the
|
|
114
|
+
* display value, so scales can change in project.json without
|
|
115
|
+
* invalidating historical records.
|
|
116
|
+
*/
|
|
117
|
+
export const rawToDisplay = (raw: any, scale: number | undefined): any => {
|
|
118
|
+
if (!scale || scale === 1) return raw;
|
|
119
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) return raw;
|
|
120
|
+
return raw * scale;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const displayToRaw = (display: any, scale: number | undefined): any => {
|
|
124
|
+
if (!scale || scale === 1) return display;
|
|
125
|
+
if (typeof display !== 'number' || !Number.isFinite(display)) return display;
|
|
126
|
+
return display / scale;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Field label with `[units]` appended when declared. */
|
|
130
|
+
export const labelOf = (f: TestFieldDef): string => {
|
|
131
|
+
const base = f.label && f.label.length > 0 ? f.label : f.name;
|
|
132
|
+
return f.units ? `${base} [${f.units}]` : base;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const hasDescription = (f: TestFieldDef): boolean =>
|
|
136
|
+
typeof f.description === 'string' && f.description.length > 0;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* "Not entered." Mirrors the server's `cycle_config_value_is_empty`
|
|
140
|
+
* (autocore-tis `config.rs`) so the HMI gate and the GM gate can never
|
|
141
|
+
* disagree about whether a field is filled in: absent, null, or an
|
|
142
|
+
* empty/whitespace string. Numeric zero and `false` are entries.
|
|
143
|
+
*/
|
|
144
|
+
export const fieldValueIsEmpty = (v: any): boolean =>
|
|
145
|
+
v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Range check for a numeric field's stored RAW value against its declared
|
|
149
|
+
* `min`/`max` (authored in DISPLAY units). Returns a short human-readable
|
|
150
|
+
* reason ("must be ≥ 5") when out of range, or null when in range / not
|
|
151
|
+
* applicable (empty value, no bounds, or non-numeric).
|
|
152
|
+
*/
|
|
153
|
+
export const rangeIssue = (f: TestFieldDef, raw: any): string | null => {
|
|
154
|
+
if (fieldValueIsEmpty(raw) || raw === '') return null;
|
|
155
|
+
if (f.min == null && f.max == null) return null;
|
|
156
|
+
const disp = Number(rawToDisplay(Number(raw), f.scale));
|
|
157
|
+
if (!Number.isFinite(disp)) return null;
|
|
158
|
+
// Bounds form an unordered interval — an author may write the range in
|
|
159
|
+
// magnitude order (min:-50, max:-1000), which is numerically inverted.
|
|
160
|
+
const lo = f.min != null && f.max != null ? Math.min(f.min, f.max) : f.min;
|
|
161
|
+
const hi = f.min != null && f.max != null ? Math.max(f.min, f.max) : f.max;
|
|
162
|
+
if (lo != null && disp < lo) return `must be ≥ ${lo}`;
|
|
163
|
+
if (hi != null && disp > hi) return `must be ≤ ${hi}`;
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The single validity rule for an operator-entered field: required-and-empty,
|
|
169
|
+
* or out of range. Returns a human-readable reason, or null when the value
|
|
170
|
+
* is acceptable. Both forms build their issue lists and their per-field red
|
|
171
|
+
* markers from this, so the summary dialog and the field icons always agree.
|
|
172
|
+
*/
|
|
173
|
+
export const fieldIssue = (f: TestFieldDef, raw: any): string | null => {
|
|
174
|
+
if (f.required && fieldValueIsEmpty(raw)) return 'is required';
|
|
175
|
+
return rangeIssue(f, raw);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const fieldIsValid = (f: TestFieldDef, raw: any): boolean =>
|
|
179
|
+
fieldIssue(f, raw) === null;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Normalise a field's `options` (bare scalars and/or `{label, value}`
|
|
183
|
+
* pairs) into the `{ label, value }[]` shape PrimeReact's Dropdown wants.
|
|
184
|
+
* Bare scalars use their stringified form as the label.
|
|
185
|
+
*/
|
|
186
|
+
export const normalizeOptions = (
|
|
187
|
+
opts: TestFieldDef['options'],
|
|
188
|
+
): { label: string; value: string | number | boolean }[] =>
|
|
189
|
+
(opts ?? []).map((o) =>
|
|
190
|
+
o !== null && typeof o === 'object'
|
|
191
|
+
? { label: String(o.label ?? o.value), value: o.value }
|
|
192
|
+
: { label: String(o), value: o },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
// Rendering
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export interface TestFieldRowProps {
|
|
200
|
+
field: TestFieldDef;
|
|
201
|
+
/** Stored RAW value. */
|
|
202
|
+
value: any;
|
|
203
|
+
/** Receives the RAW value (the row converts from display for you). */
|
|
204
|
+
onChange: (raw: any) => void;
|
|
205
|
+
/** Marks the control invalid. Defaults to `fieldIsValid(field, value)`. */
|
|
206
|
+
valid?: boolean;
|
|
207
|
+
/** Read-only rendering — used while a cycle is in flight and its
|
|
208
|
+
* annotation is already latched. */
|
|
209
|
+
disabled?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Replaces the schema-derived input for this field. Used by
|
|
212
|
+
* <TestSetupForm> for config fields an AMS `asset_ref` claims, where
|
|
213
|
+
* the picker is sourced from the asset registry rather than the field
|
|
214
|
+
* declaration.
|
|
215
|
+
*/
|
|
216
|
+
control?: React.ReactNode;
|
|
217
|
+
/** Opens the host form's shared info dialog. When omitted, fields with
|
|
218
|
+
* a `description` render no info affordance. */
|
|
219
|
+
onInfo?: (field: TestFieldDef) => void;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* One row of a 4-column `ac-form-grid`: label, control, info button,
|
|
224
|
+
* validity icon. Returns a Fragment, so the caller owns the grid.
|
|
225
|
+
*/
|
|
226
|
+
export const TestFieldRow: React.FC<TestFieldRowProps> = ({
|
|
227
|
+
field, value, onChange, valid, disabled, control, onInfo,
|
|
228
|
+
}) => {
|
|
229
|
+
const ok = valid ?? fieldIsValid(field, value);
|
|
230
|
+
const dropdownOptions = !control && Array.isArray(field.options) && field.options.length > 0
|
|
231
|
+
? normalizeOptions(field.options)
|
|
232
|
+
: null;
|
|
233
|
+
const isNum = !control && !dropdownOptions
|
|
234
|
+
&& field.type !== 'string' && field.type !== 'bool';
|
|
235
|
+
|
|
236
|
+
const input = control ?? (dropdownOptions ? (
|
|
237
|
+
<Dropdown
|
|
238
|
+
value={value ?? null}
|
|
239
|
+
options={dropdownOptions}
|
|
240
|
+
onChange={(e) => onChange(e.value)}
|
|
241
|
+
placeholder={`Select ${field.label ?? field.name}…`}
|
|
242
|
+
className={`ac-dropdown-clearable${!ok ? ' p-invalid' : ''}`}
|
|
243
|
+
showClear={!field.required}
|
|
244
|
+
disabled={disabled}
|
|
245
|
+
/>
|
|
246
|
+
) : isNum ? (
|
|
247
|
+
<ValueInput
|
|
248
|
+
label={undefined}
|
|
249
|
+
// Storage is RAW; render the DISPLAY value so the operator sees
|
|
250
|
+
// the units they configured. The change handler runs the inverse
|
|
251
|
+
// before committing back to storage.
|
|
252
|
+
value={value != null ? Number(rawToDisplay(Number(value), field.scale)) : null}
|
|
253
|
+
// min/max are authored in display units, matching the value
|
|
254
|
+
// rendered above — ValueInput rejects out-of-range entries.
|
|
255
|
+
min={field.min}
|
|
256
|
+
max={field.max}
|
|
257
|
+
onValueChanged={(val) => onChange(displayToRaw(val, field.scale))}
|
|
258
|
+
className={!ok ? 'p-invalid' : ''}
|
|
259
|
+
disabled={disabled}
|
|
260
|
+
/>
|
|
261
|
+
) : (
|
|
262
|
+
<TextInput
|
|
263
|
+
label={undefined}
|
|
264
|
+
value={value != null ? String(value) : ''}
|
|
265
|
+
onValueChanged={(val) => onChange(val)}
|
|
266
|
+
className={!ok ? 'p-invalid' : ''}
|
|
267
|
+
disabled={disabled}
|
|
268
|
+
/>
|
|
269
|
+
));
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<React.Fragment>
|
|
273
|
+
<span className="ac-form-label">{labelOf(field)}</span>
|
|
274
|
+
{input}
|
|
275
|
+
{onInfo && hasDescription(field) ? (
|
|
276
|
+
<Button
|
|
277
|
+
icon="pi pi-info-circle"
|
|
278
|
+
text
|
|
279
|
+
rounded
|
|
280
|
+
aria-label={`About ${labelOf(field)}`}
|
|
281
|
+
// Replaces a hover Tooltip — touch-friendly. The
|
|
282
|
+
// description is shown in the host form's info dialog.
|
|
283
|
+
onClick={() => onInfo(field)}
|
|
284
|
+
/>
|
|
285
|
+
) : (
|
|
286
|
+
<span aria-hidden="true" />
|
|
287
|
+
)}
|
|
288
|
+
<span style={{
|
|
289
|
+
color: ok ? 'var(--green-500)' : 'var(--red-500)',
|
|
290
|
+
display: 'flex',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
}}>
|
|
293
|
+
<i className={ok ? 'pi pi-check' : 'pi pi-times'} />
|
|
294
|
+
</span>
|
|
295
|
+
</React.Fragment>
|
|
296
|
+
);
|
|
297
|
+
};
|