@adcops/autocore-react 3.3.85 → 3.3.89

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.
Files changed (99) hide show
  1. package/dist/assets/AxisC.d.ts +4 -0
  2. package/dist/assets/AxisC.d.ts.map +1 -0
  3. package/dist/assets/AxisC.js +1 -0
  4. package/dist/assets/AxisX.js +1 -1
  5. package/dist/assets/AxisY.js +1 -1
  6. package/dist/assets/AxisZ.js +1 -1
  7. package/dist/components/ValueInput.css +9 -12
  8. package/dist/components/ValueInput.d.ts +45 -154
  9. package/dist/components/ValueInput.d.ts.map +1 -1
  10. package/dist/components/ValueInput.js +1 -1
  11. package/dist/components/ams/AmsProvider.d.ts +10 -0
  12. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  13. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  14. package/dist/components/ams/AssetRegistryTable.js +1 -1
  15. package/dist/components/forms/FormRow.d.ts +20 -0
  16. package/dist/components/forms/FormRow.d.ts.map +1 -0
  17. package/dist/components/forms/FormRow.js +1 -0
  18. package/dist/components/forms/FormSection.d.ts +19 -0
  19. package/dist/components/forms/FormSection.d.ts.map +1 -0
  20. package/dist/components/forms/FormSection.js +1 -0
  21. package/dist/components/forms/forms.css +89 -0
  22. package/dist/components/forms/index.d.ts +3 -0
  23. package/dist/components/forms/index.d.ts.map +1 -0
  24. package/dist/components/forms/index.js +1 -0
  25. package/dist/components/tis-editor/TisConfigEditor.css +121 -0
  26. package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
  27. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
  28. package/dist/components/tis-editor/TisConfigEditor.js +1 -0
  29. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
  30. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
  31. package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
  32. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
  33. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
  34. package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
  35. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
  36. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
  37. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
  38. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
  39. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
  40. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
  41. package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
  42. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
  43. package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
  44. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
  45. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
  46. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
  47. package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
  48. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
  49. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
  50. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
  51. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
  52. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
  53. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
  54. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
  55. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
  56. package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
  57. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
  58. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
  59. package/dist/components/tis-editor/types.d.ts +78 -0
  60. package/dist/components/tis-editor/types.d.ts.map +1 -0
  61. package/dist/components/tis-editor/types.js +1 -0
  62. package/dist/components/tis-editor/validation.d.ts +20 -0
  63. package/dist/components/tis-editor/validation.d.ts.map +1 -0
  64. package/dist/components/tis-editor/validation.js +1 -0
  65. package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
  66. package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
  67. package/dist/hooks/useAmsAssetTypes.js +1 -0
  68. package/dist/hooks/useTisConfig.d.ts +51 -0
  69. package/dist/hooks/useTisConfig.d.ts.map +1 -0
  70. package/dist/hooks/useTisConfig.js +1 -0
  71. package/package.json +9 -3
  72. package/src/assets/AxisC.tsx +38 -0
  73. package/src/assets/AxisX.tsx +32 -32
  74. package/src/assets/AxisY.tsx +34 -34
  75. package/src/assets/AxisZ.tsx +31 -31
  76. package/src/components/ValueInput.css +9 -12
  77. package/src/components/ValueInput.tsx +132 -317
  78. package/src/components/ams/AmsProvider.tsx +10 -0
  79. package/src/components/ams/AssetRegistryTable.tsx +53 -8
  80. package/src/components/forms/FormRow.tsx +37 -0
  81. package/src/components/forms/FormSection.tsx +39 -0
  82. package/src/components/forms/forms.css +89 -0
  83. package/src/components/forms/index.ts +2 -0
  84. package/src/components/tis-editor/TisConfigEditor.css +121 -0
  85. package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
  86. package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
  87. package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
  88. package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
  89. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
  90. package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
  91. package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
  92. package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
  93. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
  94. package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
  95. package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
  96. package/src/components/tis-editor/types.ts +95 -0
  97. package/src/components/tis-editor/validation.ts +104 -0
  98. package/src/hooks/useAmsAssetTypes.ts +70 -0
  99. package/src/hooks/useTisConfig.ts +164 -0
@@ -0,0 +1,187 @@
1
+ import { useState } from 'react';
2
+ import { Dialog } from 'primereact/dialog';
3
+ import { Button } from 'primereact/button';
4
+ import { InputText } from 'primereact/inputtext';
5
+ import { InputTextarea } from 'primereact/inputtextarea';
6
+ import { Dropdown } from 'primereact/dropdown';
7
+ import { DataTable } from 'primereact/datatable';
8
+ import { Column } from 'primereact/column';
9
+ import { FormSection } from '../../forms/FormSection';
10
+ import { FormRow } from '../../forms/FormRow';
11
+ import type { AssetRef, TestMethod } from '../types';
12
+
13
+ const SELECT_OPTIONS = [
14
+ { label: 'By location', value: 'by_location' },
15
+ { label: 'By id field', value: 'by_id_field' },
16
+ ];
17
+
18
+ const CALIBRATION_OPTIONS = [
19
+ { label: 'Ignore', value: 'ignore' },
20
+ { label: 'Warn (default)', value: 'warn' },
21
+ { label: 'Require', value: 'require' },
22
+ ];
23
+
24
+ export interface AssetRefsEditorProps {
25
+ method: TestMethod;
26
+ onChange: (next: TestMethod) => void;
27
+ /** Asset types known to AMS, supplied by the host. Empty array = use a
28
+ * free-form text field. Phase 3 wires this up to ams.list_schemas. */
29
+ knownAssetTypes?: string[];
30
+ }
31
+
32
+ const blank = (): AssetRef => ({
33
+ field: '', asset_type: '', select: 'by_location',
34
+ calibration_required: 'warn',
35
+ });
36
+
37
+ export const AssetRefsEditor: React.FC<AssetRefsEditorProps> = ({ method, onChange, knownAssetTypes = [] }) => {
38
+ const refs: AssetRef[] = (method.asset_refs as AssetRef[]) ?? [];
39
+ const [dialogOpen, setDialogOpen] = useState(false);
40
+ const [editingIdx, setEditingIdx] = useState<number | null>(null);
41
+ const [draft, setDraft] = useState<AssetRef>(blank());
42
+ const [error, setError] = useState<string | null>(null);
43
+
44
+ const openNew = () => {
45
+ setEditingIdx(null);
46
+ setDraft(blank());
47
+ setError(null);
48
+ setDialogOpen(true);
49
+ };
50
+ const openEdit = (i: number) => {
51
+ setEditingIdx(i);
52
+ setDraft({ ...refs[i] });
53
+ setError(null);
54
+ setDialogOpen(true);
55
+ };
56
+
57
+ const validate = (a: AssetRef): string | null => {
58
+ if (!a.field.trim()) return 'Field name is required.';
59
+ if (!a.asset_type.trim()) return 'Asset type is required.';
60
+ if (a.select === 'by_location' && !a.location?.trim()) {
61
+ return 'Location is required when select=by_location.';
62
+ }
63
+ if (a.select === 'by_id_field' && !a.from?.trim()) {
64
+ return 'From-path is required when select=by_id_field.';
65
+ }
66
+ const dupOfOther = refs.some((r, i) => r.field === a.field && i !== editingIdx);
67
+ if (dupOfOther) return `An asset_ref for field "${a.field}" already exists.`;
68
+ return null;
69
+ };
70
+
71
+ const handleSave = () => {
72
+ const err = validate(draft);
73
+ if (err) { setError(err); return; }
74
+ const next = [...refs];
75
+ if (editingIdx === null) next.push(draft);
76
+ else next[editingIdx] = draft;
77
+ onChange({ ...method, asset_refs: next });
78
+ setDialogOpen(false);
79
+ };
80
+
81
+ const handleRemove = (i: number) => {
82
+ if (!window.confirm(`Remove asset_ref "${refs[i].field}"?`)) return;
83
+ onChange({ ...method, asset_refs: refs.filter((_, idx) => idx !== i) });
84
+ };
85
+
86
+ const rowActions = (_r: AssetRef, opts: { rowIndex: number }) => (
87
+ <div style={{ display: 'flex', gap: '0.25rem' }}>
88
+ <Button icon="pi pi-pencil" className="p-button-text p-button-sm" onClick={() => openEdit(opts.rowIndex)} />
89
+ <Button icon="pi pi-trash" className="p-button-text p-button-danger p-button-sm" onClick={() => handleRemove(opts.rowIndex)} />
90
+ </div>
91
+ );
92
+
93
+ const assetTypeOptions = knownAssetTypes.length > 0
94
+ ? knownAssetTypes.map(t => ({ label: t, value: t }))
95
+ : null;
96
+
97
+ return (
98
+ <>
99
+ <FormSection
100
+ title="Asset References"
101
+ description="AMS dependencies resolved at start_test time and snapshotted into test.json."
102
+ actions={<Button label="Add asset_ref" icon="pi pi-plus" size="small" onClick={openNew} />}
103
+ >
104
+ <DataTable value={refs} dataKey="field" emptyMessage="No asset_refs declared.">
105
+ <Column field="field" header="Field" />
106
+ <Column field="asset_type" header="Asset type" />
107
+ <Column field="select" header="Select" style={{ width: '7rem' }} />
108
+ <Column
109
+ header="Locator"
110
+ body={(r: AssetRef) => r.select === 'by_location' ? r.location : r.from}
111
+ />
112
+ <Column field="calibration_required" header="Calibration" style={{ width: '7rem' }} />
113
+ <Column header="" body={rowActions} style={{ width: '6rem' }} />
114
+ </DataTable>
115
+ </FormSection>
116
+
117
+ <Dialog
118
+ header={editingIdx === null ? 'New asset_ref' : `Edit asset_ref: ${refs[editingIdx ?? 0]?.field}`}
119
+ visible={dialogOpen}
120
+ onHide={() => setDialogOpen(false)}
121
+ style={{ width: '40rem' }}
122
+ >
123
+ {error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
124
+ <FormRow label="Field" required hint="Key under test.json::asset_snapshot.">
125
+ <InputText value={draft.field} onChange={(e) => setDraft({ ...draft, field: e.target.value })} />
126
+ </FormRow>
127
+ <FormRow label="Asset type" required>
128
+ {assetTypeOptions ? (
129
+ <Dropdown
130
+ value={draft.asset_type}
131
+ options={assetTypeOptions}
132
+ onChange={(e) => setDraft({ ...draft, asset_type: e.value })}
133
+ editable
134
+ />
135
+ ) : (
136
+ <InputText
137
+ value={draft.asset_type}
138
+ onChange={(e) => setDraft({ ...draft, asset_type: e.target.value })}
139
+ placeholder="e.g. load_cell"
140
+ />
141
+ )}
142
+ </FormRow>
143
+ <FormRow label="Select" required>
144
+ <Dropdown
145
+ value={draft.select}
146
+ options={SELECT_OPTIONS}
147
+ onChange={(e) => setDraft({ ...draft, select: e.value })}
148
+ />
149
+ </FormRow>
150
+ {draft.select === 'by_location' && (
151
+ <FormRow label="Location" required hint="AMS location key (e.g. tsdr).">
152
+ <InputText value={draft.location ?? ''} onChange={(e) => setDraft({ ...draft, location: e.target.value })} />
153
+ </FormRow>
154
+ )}
155
+ {draft.select === 'by_id_field' && (
156
+ <FormRow label="From" required hint="Dotted config path (e.g. config.surface_asset_id).">
157
+ <InputText value={draft.from ?? ''} onChange={(e) => setDraft({ ...draft, from: e.target.value })} />
158
+ </FormRow>
159
+ )}
160
+ <FormRow label="Calibration policy">
161
+ <Dropdown
162
+ value={draft.calibration_required ?? 'warn'}
163
+ options={CALIBRATION_OPTIONS}
164
+ onChange={(e) => setDraft({ ...draft, calibration_required: e.value })}
165
+ />
166
+ </FormRow>
167
+ <FormRow label="Label">
168
+ <InputText
169
+ value={draft.label ?? ''}
170
+ onChange={(e) => setDraft({ ...draft, label: e.target.value || undefined })}
171
+ />
172
+ </FormRow>
173
+ <FormRow label="Description">
174
+ <InputTextarea
175
+ rows={2}
176
+ value={draft.description ?? ''}
177
+ onChange={(e) => setDraft({ ...draft, description: e.target.value || undefined })}
178
+ />
179
+ </FormRow>
180
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
181
+ <Button label="Cancel" className="p-button-text" onClick={() => setDialogOpen(false)} />
182
+ <Button label="Save" onClick={handleSave} />
183
+ </div>
184
+ </Dialog>
185
+ </>
186
+ );
187
+ };
@@ -0,0 +1,170 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Dialog } from 'primereact/dialog';
3
+ import { Button } from 'primereact/button';
4
+ import { InputText } from 'primereact/inputtext';
5
+ import { Dropdown } from 'primereact/dropdown';
6
+ import { FormRow } from '../../forms/FormRow';
7
+ import type { ChartAxis, ChartSeries, ChartView } from '../types';
8
+
9
+ const VIEW_TYPES = [
10
+ { label: 'Cycle scatter', value: 'cycle_scatter' },
11
+ { label: 'Raw trace', value: 'raw_trace' },
12
+ ];
13
+
14
+ const Y_AXES = [
15
+ { label: 'Left', value: 'left' },
16
+ { label: 'Right', value: 'right' },
17
+ ];
18
+
19
+ export interface ChartViewDialogProps {
20
+ visible: boolean;
21
+ initial: { viewId: string; view: ChartView } | null;
22
+ onCancel: () => void;
23
+ onSave: (viewId: string, view: ChartView) => void;
24
+ /** Known field/column names; offered as datalist suggestions. */
25
+ knownKeys: string[];
26
+ /** Other view ids already in use — for uniqueness validation. */
27
+ siblingIds: string[];
28
+ }
29
+
30
+ const blank = (): ChartView => ({
31
+ title: '', type: 'cycle_scatter',
32
+ x: { field: '', label: '' },
33
+ y: [],
34
+ });
35
+
36
+ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
37
+ visible, initial, onCancel, onSave, knownKeys, siblingIds,
38
+ }) => {
39
+ const [viewId, setViewId] = useState<string>('');
40
+ const [draft, setDraft] = useState<ChartView>(blank());
41
+ const [error, setError] = useState<string | null>(null);
42
+
43
+ useEffect(() => {
44
+ if (visible) {
45
+ setViewId(initial?.viewId ?? '');
46
+ setDraft(initial ? JSON.parse(JSON.stringify(initial.view)) : blank());
47
+ setError(null);
48
+ }
49
+ }, [visible, initial]);
50
+
51
+ const validate = (): string | null => {
52
+ if (!viewId.trim()) return 'View ID is required.';
53
+ if (siblingIds.includes(viewId) && viewId !== initial?.viewId) {
54
+ return `A view named "${viewId}" already exists.`;
55
+ }
56
+ return null;
57
+ };
58
+
59
+ const handleSave = () => {
60
+ const err = validate();
61
+ if (err) { setError(err); return; }
62
+ onSave(viewId, draft);
63
+ };
64
+
65
+ // Choose which axis property to bind to based on view type.
66
+ const isRawTrace = draft.type === 'raw_trace';
67
+ const axisKey: 'column' | 'field' = isRawTrace ? 'column' : 'field';
68
+
69
+ const setAxis = (patch: Partial<ChartAxis>) => {
70
+ setDraft({ ...draft, x: { ...draft.x, ...patch } });
71
+ };
72
+ const setSeries = (i: number, patch: Partial<ChartSeries>) => {
73
+ const next = [...draft.y];
74
+ next[i] = { ...next[i], ...patch };
75
+ setDraft({ ...draft, y: next });
76
+ };
77
+ const addSeries = () => {
78
+ setDraft({ ...draft, y: [...draft.y, { [axisKey]: '', label: '', y_axis: 'left' } as ChartSeries] });
79
+ };
80
+ const removeSeries = (i: number) => {
81
+ setDraft({ ...draft, y: draft.y.filter((_, idx) => idx !== i) });
82
+ };
83
+
84
+ return (
85
+ <Dialog
86
+ header={initial ? `Edit view: ${initial.viewId}` : 'New chart view'}
87
+ visible={visible}
88
+ onHide={onCancel}
89
+ style={{ width: '42rem' }}
90
+ >
91
+ {error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
92
+ <FormRow label="View ID" required hint="Stable key (e.g. cof_scatter). Cannot collide with other views.">
93
+ <InputText value={viewId} onChange={(e) => setViewId(e.target.value)} />
94
+ </FormRow>
95
+ <FormRow label="Title">
96
+ <InputText value={draft.title ?? ''} onChange={(e) => setDraft({ ...draft, title: e.target.value })} />
97
+ </FormRow>
98
+ <FormRow label="Type" required>
99
+ <Dropdown
100
+ value={draft.type}
101
+ options={VIEW_TYPES}
102
+ onChange={(e) => setDraft({ ...draft, type: e.value })}
103
+ />
104
+ </FormRow>
105
+ <FormRow label={isRawTrace ? 'X column' : 'X field'} required>
106
+ <InputText
107
+ value={(draft.x[axisKey] as string) ?? ''}
108
+ onChange={(e) => setAxis({ [axisKey]: e.target.value } as ChartAxis)}
109
+ list="known-keys"
110
+ />
111
+ </FormRow>
112
+ <FormRow label="X label">
113
+ <InputText
114
+ value={draft.x.label ?? ''}
115
+ onChange={(e) => setAxis({ label: e.target.value })}
116
+ />
117
+ </FormRow>
118
+
119
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '1rem 0 0.5rem' }}>
120
+ <strong>Y series</strong>
121
+ <Button label="Add series" icon="pi pi-plus" size="small" onClick={addSeries} />
122
+ </div>
123
+ {draft.y.length === 0 && <small>No series — add at least one.</small>}
124
+ {draft.y.map((s, i) => (
125
+ <div
126
+ key={i}
127
+ style={{
128
+ display: 'grid',
129
+ gridTemplateColumns: '1fr 1fr 6rem 2rem',
130
+ gap: '0.5rem',
131
+ marginBottom: '0.25rem',
132
+ alignItems: 'center',
133
+ }}
134
+ >
135
+ <InputText
136
+ placeholder={isRawTrace ? 'column' : 'field'}
137
+ value={(s[axisKey] as string) ?? ''}
138
+ onChange={(e) => setSeries(i, { [axisKey]: e.target.value } as ChartSeries)}
139
+ list="known-keys"
140
+ />
141
+ <InputText
142
+ placeholder="label"
143
+ value={s.label ?? ''}
144
+ onChange={(e) => setSeries(i, { label: e.target.value })}
145
+ />
146
+ <Dropdown
147
+ value={s.y_axis ?? 'left'}
148
+ options={Y_AXES}
149
+ onChange={(e) => setSeries(i, { y_axis: e.value })}
150
+ />
151
+ <Button
152
+ icon="pi pi-trash"
153
+ className="p-button-text p-button-danger p-button-sm"
154
+ onClick={() => removeSeries(i)}
155
+ aria-label="Remove series"
156
+ />
157
+ </div>
158
+ ))}
159
+
160
+ <datalist id="known-keys">
161
+ {knownKeys.map((k) => <option key={k} value={k} />)}
162
+ </datalist>
163
+
164
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
165
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} />
166
+ <Button label="Save" onClick={handleSave} />
167
+ </div>
168
+ </Dialog>
169
+ );
170
+ };
@@ -0,0 +1,131 @@
1
+ import { useState } from 'react';
2
+ import { DataTable } from 'primereact/datatable';
3
+ import { Column } from 'primereact/column';
4
+ import { Button } from 'primereact/button';
5
+ import { FormSection } from '../../forms/FormSection';
6
+ import { TestFieldDialog } from './TestFieldDialog';
7
+ import type { TestField, TestMethod, FieldArrayKey } from '../types';
8
+
9
+ const TITLES: Record<FieldArrayKey, string> = {
10
+ project_fields: 'Project Fields',
11
+ config_fields: 'Config Fields',
12
+ cycle_fields: 'Cycle Fields',
13
+ results_fields: 'Results Fields',
14
+ };
15
+
16
+ const DESCRIPTIONS: Record<FieldArrayKey, string> = {
17
+ project_fields: 'System-level fields (operator, station). Filled by the HMI but not cycled.',
18
+ config_fields: 'Operator-input config (speeds, loads). Snapshotted into test.json on start.',
19
+ cycle_fields: 'Per-cycle capture. One row appended to cycles.jsonl per cycle.',
20
+ results_fields: 'Post-test summary (min/max/avg, pass/fail). Written once at finish.',
21
+ };
22
+
23
+ export interface FieldArrayEditorProps {
24
+ arrayKey: FieldArrayKey;
25
+ method: TestMethod;
26
+ onChange: (next: TestMethod) => void;
27
+ }
28
+
29
+ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, method, onChange }) => {
30
+ const fields: TestField[] = (method[arrayKey] as TestField[]) ?? [];
31
+ const [dialogOpen, setDialogOpen] = useState(false);
32
+ const [editingIdx, setEditingIdx] = useState<number | null>(null);
33
+
34
+ const handleAdd = () => {
35
+ setEditingIdx(null);
36
+ setDialogOpen(true);
37
+ };
38
+
39
+ const handleEdit = (idx: number) => {
40
+ setEditingIdx(idx);
41
+ setDialogOpen(true);
42
+ };
43
+
44
+ const handleSaveField = (f: TestField) => {
45
+ const next = [...fields];
46
+ if (editingIdx === null) {
47
+ next.push(f);
48
+ } else {
49
+ next[editingIdx] = f;
50
+ }
51
+ onChange({ ...method, [arrayKey]: next });
52
+ setDialogOpen(false);
53
+ };
54
+
55
+ const handleRemove = (idx: number) => {
56
+ const target = fields[idx];
57
+ if (!target) return;
58
+ if (!window.confirm(`Remove field "${target.name}"?`)) return;
59
+ const next = fields.filter((_, i) => i !== idx);
60
+ onChange({ ...method, [arrayKey]: next });
61
+ };
62
+
63
+ const handleMove = (idx: number, dir: -1 | 1) => {
64
+ const j = idx + dir;
65
+ if (j < 0 || j >= fields.length) return;
66
+ const next = [...fields];
67
+ [next[idx], next[j]] = [next[j], next[idx]];
68
+ onChange({ ...method, [arrayKey]: next });
69
+ };
70
+
71
+ const rowActions = (_row: TestField, opts: { rowIndex: number }) => (
72
+ <div style={{ display: 'flex', gap: '0.25rem' }}>
73
+ <Button
74
+ icon="pi pi-arrow-up"
75
+ className="p-button-text p-button-sm"
76
+ disabled={opts.rowIndex === 0}
77
+ onClick={() => handleMove(opts.rowIndex, -1)}
78
+ aria-label="Move up"
79
+ />
80
+ <Button
81
+ icon="pi pi-arrow-down"
82
+ className="p-button-text p-button-sm"
83
+ disabled={opts.rowIndex === fields.length - 1}
84
+ onClick={() => handleMove(opts.rowIndex, 1)}
85
+ aria-label="Move down"
86
+ />
87
+ <Button
88
+ icon="pi pi-pencil"
89
+ className="p-button-text p-button-sm"
90
+ onClick={() => handleEdit(opts.rowIndex)}
91
+ aria-label="Edit"
92
+ />
93
+ <Button
94
+ icon="pi pi-trash"
95
+ className="p-button-text p-button-danger p-button-sm"
96
+ onClick={() => handleRemove(opts.rowIndex)}
97
+ aria-label="Remove"
98
+ />
99
+ </div>
100
+ );
101
+
102
+ return (
103
+ <>
104
+ <FormSection
105
+ title={TITLES[arrayKey]}
106
+ description={DESCRIPTIONS[arrayKey]}
107
+ actions={<Button label="Add field" icon="pi pi-plus" size="small" onClick={handleAdd} />}
108
+ >
109
+ <DataTable value={fields} dataKey="name" emptyMessage="No fields defined.">
110
+ <Column field="name" header="Name" />
111
+ <Column field="type" header="Type" style={{ width: '6rem' }} />
112
+ <Column field="units" header="Units" style={{ width: '6rem' }} />
113
+ <Column
114
+ header="Req"
115
+ body={(r: TestField) => r.required ? '✓' : ''}
116
+ style={{ width: '4rem' }}
117
+ />
118
+ <Column field="label" header="Label" />
119
+ <Column header="" body={rowActions} style={{ width: '10rem' }} />
120
+ </DataTable>
121
+ </FormSection>
122
+ <TestFieldDialog
123
+ visible={dialogOpen}
124
+ initial={editingIdx !== null ? (fields[editingIdx] ?? null) : null}
125
+ siblingNames={fields.map(f => f.name)}
126
+ onCancel={() => setDialogOpen(false)}
127
+ onSave={handleSaveField}
128
+ />
129
+ </>
130
+ );
131
+ };
@@ -0,0 +1,36 @@
1
+ import { InputText } from 'primereact/inputtext';
2
+ import { InputTextarea } from 'primereact/inputtextarea';
3
+ import { FormSection } from '../../forms/FormSection';
4
+ import { FormRow } from '../../forms/FormRow';
5
+ import type { TestMethod } from '../types';
6
+
7
+ export interface IdentitySectionProps {
8
+ method: TestMethod;
9
+ onChange: (next: TestMethod) => void;
10
+ }
11
+
12
+ export const IdentitySection: React.FC<IdentitySectionProps> = ({ method, onChange }) => {
13
+ return (
14
+ <FormSection
15
+ title="Identity"
16
+ description="Display label and operator-facing description for this method."
17
+ >
18
+ <FormRow label="Label" hint="Pretty name shown in the Test Method picker.">
19
+ <InputText
20
+ value={(method.label as string) ?? ''}
21
+ onChange={(e) => onChange({ ...method, label: e.target.value })}
22
+ />
23
+ </FormRow>
24
+ <FormRow
25
+ label="Description"
26
+ hint="Long-form guidance shown beneath the picker when this method is selected."
27
+ >
28
+ <InputTextarea
29
+ rows={3}
30
+ value={(method.description as string) ?? ''}
31
+ onChange={(e) => onChange({ ...method, description: e.target.value })}
32
+ />
33
+ </FormRow>
34
+ </FormSection>
35
+ );
36
+ };