@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
|
@@ -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;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the same tabbed layout and scroll behaviour without copy-pasting a
|
|
7
7
|
* per-project `TestInformationView`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Up to three tabs, all self-driving from <TisProvider> (no prop threading):
|
|
10
10
|
*
|
|
11
11
|
* Project tab — <ProjectSelector> stays pinned at the top; the
|
|
12
12
|
* <ResultHistoryTable> (Test History) scrolls beneath
|
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
* Test tab — <TestSetupForm>. Its "Test Setup" header (Sample ID,
|
|
15
15
|
* Test Method, Configuration) stays pinned; the "Test
|
|
16
16
|
* Configuration" field list scrolls when it overflows.
|
|
17
|
+
* Cycle tab — <CycleConfigForm>, the per-row annotation recorded onto
|
|
18
|
+
* each cycle. Present only when the selected method
|
|
19
|
+
* declares `cycle_config_fields`, and badged when a
|
|
20
|
+
* required entry is missing. Auto-selected when a run
|
|
21
|
+
* goes active: Test setup is a pre-run concern, the
|
|
22
|
+
* annotation is a during-run one.
|
|
23
|
+
*
|
|
24
|
+
* The Cycle tab is deliberately not the only place this state is visible —
|
|
25
|
+
* an operator about to press Start is looking at the footer, not at a tab.
|
|
26
|
+
* Put <CycleStatusStrip> beside the Start button for that.
|
|
17
27
|
*
|
|
18
28
|
* The whole view fills its parent's height and confines overflow to the
|
|
19
29
|
* inner scroll regions, so the tab strip and the section headers stay
|
|
@@ -22,11 +32,13 @@
|
|
|
22
32
|
* `min-height: 0`, or a fixed-height panel) to get that behaviour.
|
|
23
33
|
*/
|
|
24
34
|
|
|
25
|
-
import React from 'react';
|
|
35
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
26
36
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
27
37
|
import { ProjectSelector } from './ProjectSelector';
|
|
28
38
|
import { ResultHistoryTable } from './ResultHistoryTable';
|
|
29
39
|
import { TestSetupForm } from './TestSetupForm';
|
|
40
|
+
import { CycleConfigForm } from './CycleConfigForm';
|
|
41
|
+
import { useTis } from './TisProvider';
|
|
30
42
|
|
|
31
43
|
export interface TestSetupViewProps {
|
|
32
44
|
/** Extra class names applied to the root element. */
|
|
@@ -40,9 +52,25 @@ export const TestSetupView: React.FC<TestSetupViewProps> = ({ className, style }
|
|
|
40
52
|
? `ac-test-setup-view ${className}`
|
|
41
53
|
: 'ac-test-setup-view';
|
|
42
54
|
|
|
55
|
+
const tis = useTis();
|
|
56
|
+
const hasCycleConfig = tis.cycleConfigFields.length > 0;
|
|
57
|
+
const cycleIncomplete = hasCycleConfig && !tis.cycleConfig.ready;
|
|
58
|
+
|
|
59
|
+
// Which tab is showing. Controlled so the view can move the operator to
|
|
60
|
+
// the Cycle tab when a run goes active — the annotation is what they
|
|
61
|
+
// need next, and Test setup is already done by then.
|
|
62
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
63
|
+
const prevActive = useRef(false);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (tis.state.active && !prevActive.current && hasCycleConfig) {
|
|
66
|
+
setActiveIndex(2);
|
|
67
|
+
}
|
|
68
|
+
prevActive.current = tis.state.active;
|
|
69
|
+
}, [tis.state.active, hasCycleConfig]);
|
|
70
|
+
|
|
43
71
|
return (
|
|
44
72
|
<div className={rootClass} style={style}>
|
|
45
|
-
<TabView>
|
|
73
|
+
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
46
74
|
{/* Project tab: pick or create a project, then browse its
|
|
47
75
|
history. ResultHistoryTable is project-scoped (across
|
|
48
76
|
methods) by default, so this is the natural place to
|
|
@@ -66,6 +94,35 @@ export const TestSetupView: React.FC<TestSetupViewProps> = ({ className, style }
|
|
|
66
94
|
<TabPanel header="Test">
|
|
67
95
|
<TestSetupForm />
|
|
68
96
|
</TabPanel>
|
|
97
|
+
|
|
98
|
+
{/* Cycle tab: per-row operator annotation. Rendered only
|
|
99
|
+
when the method declares any — a method that records
|
|
100
|
+
nothing by hand shouldn't grow an empty tab. The header
|
|
101
|
+
carries a warning marker when a required entry is
|
|
102
|
+
missing, so the state is legible from the tab strip
|
|
103
|
+
without opening it. */}
|
|
104
|
+
{hasCycleConfig && (
|
|
105
|
+
<TabPanel
|
|
106
|
+
header={
|
|
107
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
108
|
+
Cycle
|
|
109
|
+
{cycleIncomplete && (
|
|
110
|
+
<i
|
|
111
|
+
className="pi pi-exclamation-circle"
|
|
112
|
+
style={{ color: 'var(--red-500)' }}
|
|
113
|
+
aria-label="Cycle Configuration incomplete"
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
117
|
+
}
|
|
118
|
+
>
|
|
119
|
+
<div className="ac-test-setup-view__tab">
|
|
120
|
+
<div className="ac-test-setup-view__scroll">
|
|
121
|
+
<CycleConfigForm />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</TabPanel>
|
|
125
|
+
)}
|
|
69
126
|
</TabView>
|
|
70
127
|
</div>
|
|
71
128
|
);
|