@adcops/autocore-react 3.3.115 → 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/TestDataView.d.ts +4 -0
- 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 +1 -1
- 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/TestDataView.tsx +19 -3
- 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
|
@@ -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
|
+
};
|
|
@@ -6,12 +6,26 @@ import { Dialog } from 'primereact/dialog';
|
|
|
6
6
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
7
7
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
8
8
|
import { MessageType } from '../../hub/CommandMessage';
|
|
9
|
-
import { ValueInput } from '../ValueInput';
|
|
10
9
|
import { TextInput } from '../TextInput';
|
|
11
10
|
import { useTis } from './TisProvider';
|
|
12
11
|
import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
|
|
13
12
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
14
13
|
import { ConfigurationDialog, configLabelOf } from './ConfigurationDialog';
|
|
14
|
+
import {
|
|
15
|
+
TestFieldRow,
|
|
16
|
+
displayToRaw,
|
|
17
|
+
fieldIsValid,
|
|
18
|
+
fieldIssue,
|
|
19
|
+
labelOf,
|
|
20
|
+
type CycleConfigScope,
|
|
21
|
+
type TestFieldDef,
|
|
22
|
+
} from './TestFieldRow';
|
|
23
|
+
|
|
24
|
+
// The field schema and its display/raw + validation helpers are shared with
|
|
25
|
+
// <CycleConfigForm> — see TestFieldRow.tsx. Re-exported here because this
|
|
26
|
+
// module has been the public home of `TestFieldDef` since before Cycle
|
|
27
|
+
// Configuration existed.
|
|
28
|
+
export type { TestFieldDef } from './TestFieldRow';
|
|
15
29
|
|
|
16
30
|
/**
|
|
17
31
|
* One asset_ref declared on a test method. We only consume the subset
|
|
@@ -27,64 +41,24 @@ interface TisAssetRef {
|
|
|
27
41
|
location?: string;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
export interface TestFieldDef {
|
|
31
|
-
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
32
|
-
name: string;
|
|
33
|
-
type: string;
|
|
34
|
-
units?: string;
|
|
35
|
-
required?: boolean;
|
|
36
|
-
source?: string;
|
|
37
|
-
/** Pretty label rendered by the form. Falls back to `name`. Units
|
|
38
|
-
* are appended automatically; don't pre-format `[mm]` into label. */
|
|
39
|
-
label?: string;
|
|
40
|
-
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
41
|
-
description?: string;
|
|
42
|
-
/** Seed value applied when the operator selects this test method.
|
|
43
|
-
* For source-bound fields the default is written to the GM tag so
|
|
44
|
-
* the control program sees it; for non-source fields it's stashed
|
|
45
|
-
* straight into stagedConfig. Operator edits override per-stage,
|
|
46
|
-
* but the schema default never mutates — re-selecting the method
|
|
47
|
-
* re-applies the default. **Authored in display units** when
|
|
48
|
-
* `scale` is set — the form divides by `scale` before writing to
|
|
49
|
-
* GM / stagedConfig so the underlying storage stays raw. */
|
|
50
|
-
default?: any;
|
|
51
|
-
/** Optional unit-conversion multiplier. Mirrors AutoCoreTagContext:
|
|
52
|
-
* ```
|
|
53
|
-
* display = raw * scale
|
|
54
|
-
* raw = display / scale
|
|
55
|
-
* ```
|
|
56
|
-
* Example: backend stores degrees, operator enters revolutions →
|
|
57
|
-
* `scale: 0.00277778`. None / 1.0 = no conversion.
|
|
58
|
-
*
|
|
59
|
-
* Storage stays raw — the form scales only on input and display.
|
|
60
|
-
* Cycle and results values are scaled by the corresponding paths
|
|
61
|
-
* in TestDataView; the server scales CSV exports too. */
|
|
62
|
-
scale?: number;
|
|
63
|
-
/** Optional inclusive bounds (display units, same convention as `default`
|
|
64
|
-
* and `scale`) for the operator's numeric entry. The numeric input
|
|
65
|
-
* rejects values outside `[min, max]`; non-numeric fields ignore them. */
|
|
66
|
-
min?: number;
|
|
67
|
-
max?: number;
|
|
68
|
-
/** Optional fixed set of choices. When present, the field renders
|
|
69
|
-
* as a dropdown and the operator must pick one of the declared
|
|
70
|
-
* values rather than typing freely. Each entry is either a bare
|
|
71
|
-
* scalar (label === value) or an explicit `{ label, value }` pair
|
|
72
|
-
* when the displayed text should differ from the stored value.
|
|
73
|
-
* Works with any `type`; like `default`, values are authored in
|
|
74
|
-
* display units when `scale` is set. */
|
|
75
|
-
options?: Array<
|
|
76
|
-
| string
|
|
77
|
-
| number
|
|
78
|
-
| boolean
|
|
79
|
-
| { label?: string; value: string | number | boolean }
|
|
80
|
-
>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
44
|
export interface TestMethod {
|
|
84
45
|
project_fields: TestFieldDef[];
|
|
85
46
|
config_fields: TestFieldDef[];
|
|
86
47
|
cycle_fields: TestFieldDef[];
|
|
87
48
|
results_fields: TestFieldDef[];
|
|
49
|
+
/**
|
|
50
|
+
* Operator-entered per-row annotation, rendered on the Cycle tab by
|
|
51
|
+
* <CycleConfigForm> rather than by this form. Declared here so a
|
|
52
|
+
* generated schema literal typechecks against `TestMethod`.
|
|
53
|
+
*/
|
|
54
|
+
cycle_config_fields?: TestFieldDef[];
|
|
55
|
+
/** Which boundary the operator annotates at: `'run'` (default — one
|
|
56
|
+
* latch per Start, stamping every cycle of the run) or `'cycle'`
|
|
57
|
+
* (one latch per cycle, re-gated between cycles). */
|
|
58
|
+
cycle_config_scope?: CycleConfigScope;
|
|
59
|
+
/** Open the Cycle Configuration dialog at the boundary instead of
|
|
60
|
+
* relying on the operator having filled the Cycle tab. */
|
|
61
|
+
prompt_before_cycle?: boolean;
|
|
88
62
|
/**
|
|
89
63
|
* AMS asset references resolved at start_test. The form scans these
|
|
90
64
|
* for `select=by_id_field` entries pointing at a config field
|
|
@@ -159,38 +133,6 @@ export interface TestSetupFormProps {
|
|
|
159
133
|
// Helpers
|
|
160
134
|
// -------------------------------------------------------------------------
|
|
161
135
|
|
|
162
|
-
const labelOf = (f: TestFieldDef): string => {
|
|
163
|
-
const base = f.label && f.label.length > 0 ? f.label : f.name;
|
|
164
|
-
return f.units ? `${base} [${f.units}]` : base;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Two helpers for the display ↔ raw boundary on numeric fields.
|
|
169
|
-
*
|
|
170
|
-
* Convention: `display = raw * scale`, `raw = display / scale`. Matches
|
|
171
|
-
* AutoCoreTagContext. Default scale = 1.0 (no conversion). Non-numeric
|
|
172
|
-
* input values (string, null, undefined, NaN) pass through unchanged
|
|
173
|
-
* — the form has fields of other types that share the change handler.
|
|
174
|
-
*
|
|
175
|
-
* Storage (config map, GM, on-disk JSON) always holds the raw value.
|
|
176
|
-
* Only the inputs the operator looks at and the cells in TestDataView
|
|
177
|
-
* use the display value. This way scales can change in project.json
|
|
178
|
-
* without invalidating historical records.
|
|
179
|
-
*/
|
|
180
|
-
const rawToDisplay = (raw: any, scale: number | undefined): any => {
|
|
181
|
-
if (!scale || scale === 1) return raw;
|
|
182
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw)) return raw;
|
|
183
|
-
return raw * scale;
|
|
184
|
-
};
|
|
185
|
-
const displayToRaw = (display: any, scale: number | undefined): any => {
|
|
186
|
-
if (!scale || scale === 1) return display;
|
|
187
|
-
if (typeof display !== 'number' || !Number.isFinite(display)) return display;
|
|
188
|
-
return display / scale;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const hasDescription = (f: TestFieldDef): boolean =>
|
|
192
|
-
typeof f.description === 'string' && f.description.length > 0;
|
|
193
|
-
|
|
194
136
|
/** Matches an FQDN default token like `${gm.safe_speed}` (whole-string). */
|
|
195
137
|
const DEFAULT_TOKEN_RE = /^\$\{\s*([^}]+?)\s*\}$/;
|
|
196
138
|
|
|
@@ -230,41 +172,6 @@ const resolveDefaultRaw = (
|
|
|
230
172
|
return { raw: displayToRaw(d, field.scale), ok: true };
|
|
231
173
|
};
|
|
232
174
|
|
|
233
|
-
/**
|
|
234
|
-
* Range check for a numeric field's stored RAW value against its declared
|
|
235
|
-
* `min`/`max` (authored in DISPLAY units). Returns a short human-readable
|
|
236
|
-
* reason ("must be ≥ 5") when out of range, or null when in range / not
|
|
237
|
-
* applicable (empty value, no bounds, or non-numeric). Storage is raw, so we
|
|
238
|
-
* convert to display first to compare against the author's display-unit bounds.
|
|
239
|
-
*/
|
|
240
|
-
const rangeIssue = (f: TestFieldDef, raw: any): string | null => {
|
|
241
|
-
if (raw === undefined || raw === '' || raw === null) return null;
|
|
242
|
-
if (f.min == null && f.max == null) return null;
|
|
243
|
-
const disp = Number(rawToDisplay(Number(raw), f.scale));
|
|
244
|
-
if (!Number.isFinite(disp)) return null;
|
|
245
|
-
// Bounds form an unordered interval — an author may write the range in
|
|
246
|
-
// magnitude order (min:-50, max:-1000), which is numerically inverted.
|
|
247
|
-
const lo = f.min != null && f.max != null ? Math.min(f.min, f.max) : f.min;
|
|
248
|
-
const hi = f.min != null && f.max != null ? Math.max(f.min, f.max) : f.max;
|
|
249
|
-
if (lo != null && disp < lo) return `must be ≥ ${lo}`;
|
|
250
|
-
if (hi != null && disp > hi) return `must be ≤ ${hi}`;
|
|
251
|
-
return null;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Normalise a field's `options` (bare scalars and/or `{label, value}`
|
|
256
|
-
* pairs) into the `{ label, value }[]` shape PrimeReact's Dropdown
|
|
257
|
-
* wants. Bare scalars use their stringified form as the label.
|
|
258
|
-
*/
|
|
259
|
-
const normalizeOptions = (
|
|
260
|
-
opts: TestFieldDef['options'],
|
|
261
|
-
): { label: string; value: string | number | boolean }[] =>
|
|
262
|
-
(opts ?? []).map((o) =>
|
|
263
|
-
o !== null && typeof o === 'object'
|
|
264
|
-
? { label: String(o.label ?? o.value), value: o.value }
|
|
265
|
-
: { label: String(o), value: o },
|
|
266
|
-
);
|
|
267
|
-
|
|
268
175
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
269
176
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
270
177
|
|
|
@@ -550,10 +457,7 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
550
457
|
|
|
551
458
|
for (const field of schema.config_fields) {
|
|
552
459
|
if (field.name === 'sample_id') continue;
|
|
553
|
-
|
|
554
|
-
const empty = v === undefined || v === '' || v === null;
|
|
555
|
-
if (field.required && empty) { valid = false; break; }
|
|
556
|
-
if (rangeIssue(field, v)) { valid = false; break; }
|
|
460
|
+
if (fieldIssue(field, config[field.name])) { valid = false; break; }
|
|
557
461
|
}
|
|
558
462
|
|
|
559
463
|
setIsValid(valid);
|
|
@@ -583,23 +487,15 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
583
487
|
onValidationChange, invoke,
|
|
584
488
|
]);
|
|
585
489
|
|
|
586
|
-
const isFieldValid = (field: TestFieldDef) =>
|
|
587
|
-
const v = config[field.name];
|
|
588
|
-
const empty = v === undefined || v === '' || v === null;
|
|
589
|
-
if (field.required && empty) return false;
|
|
590
|
-
if (rangeIssue(field, v)) return false;
|
|
591
|
-
return true;
|
|
592
|
-
};
|
|
490
|
+
const isFieldValid = (field: TestFieldDef) => fieldIsValid(field, config[field.name]);
|
|
593
491
|
|
|
594
492
|
const handleSampleIdChange = (value: string) => {
|
|
595
493
|
setSampleIdLocal(value);
|
|
596
494
|
};
|
|
597
495
|
|
|
598
|
-
const handleFieldChange = async (field: TestFieldDef,
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
// scales pass through unchanged via the helper.
|
|
602
|
-
const rawVal = displayToRaw(val, field.scale);
|
|
496
|
+
const handleFieldChange = async (field: TestFieldDef, rawVal: any) => {
|
|
497
|
+
// <TestFieldRow> has already converted the operator's DISPLAY entry
|
|
498
|
+
// to RAW; storage and GM always hold raw.
|
|
603
499
|
setConfig({ ...config, [field.name]: rawVal });
|
|
604
500
|
if (field.source) {
|
|
605
501
|
try { await write(field.source, rawVal); }
|
|
@@ -642,81 +538,31 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
642
538
|
const renderConfigField = (field: TestFieldDef) => {
|
|
643
539
|
if (field.name === 'sample_id') return null;
|
|
644
540
|
const valid = isFieldValid(field);
|
|
541
|
+
// A field an AMS asset_ref claims gets its picker from the registry
|
|
542
|
+
// instead of from the field declaration; everything else is rendered
|
|
543
|
+
// from the schema by <TestFieldRow>, exactly as the Cycle tab does.
|
|
645
544
|
const assetType = assetTypeForField(field);
|
|
646
|
-
// A schema-declared `options` list turns the field into a
|
|
647
|
-
// dropdown (skipped when an asset_ref already claims the field
|
|
648
|
-
// — that picker is sourced from AMS, not the static list).
|
|
649
|
-
const dropdownOptions = !assetType && Array.isArray(field.options) && field.options.length > 0
|
|
650
|
-
? normalizeOptions(field.options)
|
|
651
|
-
: null;
|
|
652
|
-
const isNum = !assetType && !dropdownOptions
|
|
653
|
-
&& field.type !== 'string' && field.type !== 'bool';
|
|
654
545
|
return (
|
|
655
|
-
<
|
|
656
|
-
|
|
657
|
-
{
|
|
546
|
+
<TestFieldRow
|
|
547
|
+
key={field.name}
|
|
548
|
+
field={field}
|
|
549
|
+
value={config[field.name]}
|
|
550
|
+
valid={valid}
|
|
551
|
+
onChange={(raw) => handleFieldChange(field, raw)}
|
|
552
|
+
onInfo={(f) => setInfoDialog({
|
|
553
|
+
open: true,
|
|
554
|
+
title: labelOf(f),
|
|
555
|
+
body: <p style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{f.description}</p>,
|
|
556
|
+
})}
|
|
557
|
+
control={assetType ? (
|
|
658
558
|
<AssetIdPicker
|
|
659
559
|
assetType={assetType}
|
|
660
560
|
value={config[field.name] != null ? String(config[field.name]) : ''}
|
|
661
561
|
onChange={(val) => handleFieldChange(field, val)}
|
|
662
562
|
invalid={!valid}
|
|
663
563
|
/>
|
|
664
|
-
) :
|
|
665
|
-
|
|
666
|
-
value={config[field.name] ?? null}
|
|
667
|
-
options={dropdownOptions}
|
|
668
|
-
onChange={(e) => handleFieldChange(field, e.value)}
|
|
669
|
-
placeholder={`Select ${field.label ?? field.name}…`}
|
|
670
|
-
className={`ac-dropdown-clearable${!valid ? ' p-invalid' : ''}`}
|
|
671
|
-
showClear={!field.required}
|
|
672
|
-
/>
|
|
673
|
-
) : isNum ? (
|
|
674
|
-
<ValueInput
|
|
675
|
-
label={undefined}
|
|
676
|
-
// Storage is RAW; render the DISPLAY value so
|
|
677
|
-
// operator sees the units they configured. The
|
|
678
|
-
// change handler runs the inverse before
|
|
679
|
-
// committing back to storage.
|
|
680
|
-
value={config[field.name] != null
|
|
681
|
-
? Number(rawToDisplay(Number(config[field.name]), field.scale))
|
|
682
|
-
: null}
|
|
683
|
-
// min/max are authored in display units, matching the
|
|
684
|
-
// value rendered above — ValueInput rejects out-of-range
|
|
685
|
-
// entries on accept.
|
|
686
|
-
min={field.min}
|
|
687
|
-
max={field.max}
|
|
688
|
-
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
689
|
-
className={!valid ? 'p-invalid' : ''}
|
|
690
|
-
/>
|
|
691
|
-
) : (
|
|
692
|
-
<TextInput
|
|
693
|
-
label={undefined}
|
|
694
|
-
value={config[field.name] != null ? String(config[field.name]) : ''}
|
|
695
|
-
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
696
|
-
className={!valid ? 'p-invalid' : ''}
|
|
697
|
-
/>
|
|
698
|
-
)}
|
|
699
|
-
{hasDescription(field) ? (
|
|
700
|
-
<Button
|
|
701
|
-
icon="pi pi-info-circle"
|
|
702
|
-
text
|
|
703
|
-
rounded
|
|
704
|
-
aria-label={`About ${labelOf(field)}`}
|
|
705
|
-
onClick={() => setInfoDialog({
|
|
706
|
-
open: true,
|
|
707
|
-
title: labelOf(field),
|
|
708
|
-
body: <p style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{field.description}</p>,
|
|
709
|
-
})}
|
|
710
|
-
// Replaces a hover Tooltip — touch-friendly. The
|
|
711
|
-
// description is shown in the shared info dialog.
|
|
712
|
-
/>
|
|
713
|
-
) : (
|
|
714
|
-
<span aria-hidden="true" />
|
|
715
|
-
)}
|
|
716
|
-
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
717
|
-
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
718
|
-
</span>
|
|
719
|
-
</React.Fragment>
|
|
564
|
+
) : undefined}
|
|
565
|
+
/>
|
|
720
566
|
);
|
|
721
567
|
};
|
|
722
568
|
|
|
@@ -772,14 +618,8 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
772
618
|
if (schema) {
|
|
773
619
|
for (const field of schema.config_fields) {
|
|
774
620
|
if (field.name === 'sample_id') continue;
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
if (field.required && empty) {
|
|
778
|
-
issues.push(`Required field "${labelOf(field)}" is empty.`);
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
const re = rangeIssue(field, v);
|
|
782
|
-
if (re) issues.push(`"${labelOf(field)}" ${re}.`);
|
|
621
|
+
const issue = fieldIssue(field, config[field.name]);
|
|
622
|
+
if (issue) issues.push(`"${labelOf(field)}" ${issue}.`);
|
|
783
623
|
}
|
|
784
624
|
}
|
|
785
625
|
return issues;
|