@adcops/autocore-react 3.3.84 → 3.3.87

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 (86) hide show
  1. package/dist/components/ValueInput.css +9 -12
  2. package/dist/components/ValueInput.d.ts +45 -154
  3. package/dist/components/ValueInput.d.ts.map +1 -1
  4. package/dist/components/ValueInput.js +1 -1
  5. package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
  6. package/dist/components/ams/AssetEditDialog.js +1 -1
  7. package/dist/components/forms/FormRow.d.ts +20 -0
  8. package/dist/components/forms/FormRow.d.ts.map +1 -0
  9. package/dist/components/forms/FormRow.js +1 -0
  10. package/dist/components/forms/FormSection.d.ts +19 -0
  11. package/dist/components/forms/FormSection.d.ts.map +1 -0
  12. package/dist/components/forms/FormSection.js +1 -0
  13. package/dist/components/forms/forms.css +89 -0
  14. package/dist/components/forms/index.d.ts +3 -0
  15. package/dist/components/forms/index.d.ts.map +1 -0
  16. package/dist/components/forms/index.js +1 -0
  17. package/dist/components/tis-editor/TisConfigEditor.css +121 -0
  18. package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
  19. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
  20. package/dist/components/tis-editor/TisConfigEditor.js +1 -0
  21. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
  22. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
  23. package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
  24. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
  25. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
  26. package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
  27. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
  28. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
  29. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
  30. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
  31. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
  32. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
  33. package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
  34. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
  35. package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
  36. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
  37. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
  38. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
  39. package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
  40. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
  41. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
  42. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
  43. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
  44. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
  45. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
  46. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
  47. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
  48. package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
  49. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
  50. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
  51. package/dist/components/tis-editor/types.d.ts +78 -0
  52. package/dist/components/tis-editor/types.d.ts.map +1 -0
  53. package/dist/components/tis-editor/types.js +1 -0
  54. package/dist/components/tis-editor/validation.d.ts +20 -0
  55. package/dist/components/tis-editor/validation.d.ts.map +1 -0
  56. package/dist/components/tis-editor/validation.js +1 -0
  57. package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
  58. package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
  59. package/dist/hooks/useAmsAssetTypes.js +1 -0
  60. package/dist/hooks/useTisConfig.d.ts +51 -0
  61. package/dist/hooks/useTisConfig.d.ts.map +1 -0
  62. package/dist/hooks/useTisConfig.js +1 -0
  63. package/package.json +9 -3
  64. package/src/components/ValueInput.css +9 -12
  65. package/src/components/ValueInput.tsx +132 -317
  66. package/src/components/ams/AssetEditDialog.tsx +357 -20
  67. package/src/components/forms/FormRow.tsx +37 -0
  68. package/src/components/forms/FormSection.tsx +39 -0
  69. package/src/components/forms/forms.css +89 -0
  70. package/src/components/forms/index.ts +2 -0
  71. package/src/components/tis-editor/TisConfigEditor.css +121 -0
  72. package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
  73. package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
  74. package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
  75. package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
  76. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
  77. package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
  78. package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
  79. package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
  80. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
  81. package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
  82. package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
  83. package/src/components/tis-editor/types.ts +95 -0
  84. package/src/components/tis-editor/validation.ts +104 -0
  85. package/src/hooks/useAmsAssetTypes.ts +70 -0
  86. package/src/hooks/useTisConfig.ts +164 -0
@@ -0,0 +1,121 @@
1
+ .tis-editor {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ }
7
+
8
+ .tis-editor__header {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ padding: 0.75rem 1rem;
13
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
14
+ }
15
+
16
+ .tis-editor__header h2 {
17
+ margin: 0;
18
+ font-size: 1.125rem;
19
+ }
20
+
21
+ .tis-editor__header-actions {
22
+ display: flex;
23
+ gap: 0.5rem;
24
+ }
25
+
26
+ .tis-editor__dirty-pill {
27
+ display: inline-block;
28
+ background: #ea580c;
29
+ color: white;
30
+ font-size: 0.7rem;
31
+ font-weight: 600;
32
+ text-transform: uppercase;
33
+ letter-spacing: 0.05em;
34
+ padding: 0.15rem 0.5rem;
35
+ border-radius: 999px;
36
+ margin-left: 0.5rem;
37
+ vertical-align: middle;
38
+ }
39
+
40
+ .tis-editor__body {
41
+ flex: 1;
42
+ display: flex;
43
+ min-height: 0;
44
+ }
45
+
46
+ .tis-editor__sidebar {
47
+ flex: 0 0 320px;
48
+ border-right: 1px solid var(--surface-d, #e2e8f0);
49
+ display: flex;
50
+ flex-direction: column;
51
+ min-height: 0;
52
+ }
53
+
54
+ .tis-editor__sidebar-actions {
55
+ display: flex;
56
+ gap: 0.25rem;
57
+ padding: 0.5rem;
58
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
59
+ flex-wrap: wrap;
60
+ }
61
+
62
+ .tis-editor__sidebar-actions .p-button {
63
+ flex: 1 1 auto;
64
+ min-width: 0;
65
+ }
66
+
67
+ .tis-editor__detail {
68
+ flex: 1;
69
+ display: flex;
70
+ flex-direction: column;
71
+ min-height: 0;
72
+ }
73
+
74
+ .tis-editor__detail-header {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ padding: 0.5rem 1rem;
79
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
80
+ background: var(--surface-b, #f8fafc);
81
+ }
82
+
83
+ .tis-editor__editor-wrap {
84
+ flex: 1;
85
+ min-height: 0;
86
+ }
87
+
88
+ .tis-editor__editor-wrap > * {
89
+ height: 100%;
90
+ }
91
+
92
+ .tis-editor__error {
93
+ background: #fef2f2;
94
+ color: #991b1b;
95
+ border-left: 3px solid #dc2626;
96
+ padding: 0.5rem 1rem;
97
+ margin: 0.5rem 1rem;
98
+ font-size: 0.875rem;
99
+ }
100
+
101
+ .tis-editor__error pre {
102
+ white-space: pre-wrap;
103
+ margin: 0.25rem 0 0 0;
104
+ font-family: ui-monospace, monospace;
105
+ font-size: 0.8rem;
106
+ }
107
+
108
+ .tis-editor__empty {
109
+ flex: 1;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ color: var(--text-color-secondary, #64748b);
114
+ }
115
+
116
+ .tis-editor__new-method-label {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.25rem;
120
+ margin-bottom: 0.25rem;
121
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * TisConfigEditor — master-detail UI for editing TIS test_methods.
3
+ *
4
+ * Left: PrimeReact DataTable listing methods (method_id + label).
5
+ * Right: tabbed editor for the selected method. Phase 1 ships a single
6
+ * JSON-via-Monaco tab; Phase 2 layers form editors on top of it.
7
+ * Action bar: New / Duplicate / Delete / Apply / Save / Revert.
8
+ *
9
+ * "Apply" pushes the local Monaco buffer to the server-side stage
10
+ * (`tis.put_method`). "Save" persists the entire stage to project.json
11
+ * (`tis.save_config`). "Revert" drops the stage (`tis.discard_config_changes`).
12
+ *
13
+ * Save is disabled (with a tooltip) when active tests are open against
14
+ * any method — the server enforces the same gate, but disabling on the
15
+ * client gives clearer UX.
16
+ */
17
+
18
+ import { useEffect, useMemo, useState } from 'react';
19
+ import { DataTable } from 'primereact/datatable';
20
+ import { Column } from 'primereact/column';
21
+ import { Button } from 'primereact/button';
22
+ import { InputText } from 'primereact/inputtext';
23
+ import { Dialog } from 'primereact/dialog';
24
+ import { useContext } from 'react';
25
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
26
+ import { MessageType } from '../../hub/CommandMessage';
27
+ import { useTisConfig, type TisIpcInvoker } from '../../hooks/useTisConfig';
28
+ import { useAmsAssetTypes } from '../../hooks/useAmsAssetTypes';
29
+ import { MethodFormEditor } from './editor/MethodFormEditor';
30
+ import { SaveDiffDialog } from './editor/SaveDiffDialog';
31
+ import type { TestMethod } from './types';
32
+
33
+ import './TisConfigEditor.css';
34
+
35
+ export interface TisConfigEditorProps {
36
+ /** Project ID. Today this is informational (the server hosts one
37
+ * project) but the wire contract carries it for forward compatibility. */
38
+ projectId: string;
39
+ /** Optional invoker override — primarily for the playground / tests. */
40
+ invoker?: TisIpcInvoker;
41
+ }
42
+
43
+ interface MethodRow {
44
+ id: string;
45
+ label: string;
46
+ }
47
+
48
+ const EMPTY_METHOD: TestMethod = {
49
+ label: '',
50
+ description: '',
51
+ project_fields: [],
52
+ config_fields: [],
53
+ cycle_fields: [],
54
+ results_fields: [],
55
+ views: {},
56
+ asset_refs: [],
57
+ };
58
+
59
+ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, invoker }) => {
60
+ const ctx = useContext(EventEmitterContext);
61
+ // Resolve invoker once so it can be passed into the SaveDiffDialog
62
+ // alongside the hook's internal use of it.
63
+ const effectiveInvoker: TisIpcInvoker = invoker
64
+ ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
65
+
66
+ const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
67
+ const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
68
+ const [selectedId, setSelectedId] = useState<string | null>(null);
69
+ const [draftError, setDraftError] = useState<string | null>(null);
70
+ const [busy, setBusy] = useState<boolean>(false);
71
+ const [saveDialogOpen, setSaveDialogOpen] = useState<boolean>(false);
72
+
73
+ // New-method dialog state.
74
+ const [newDialogOpen, setNewDialogOpen] = useState<boolean>(false);
75
+ const [newId, setNewId] = useState<string>('');
76
+
77
+ const rows: MethodRow[] = useMemo(() => {
78
+ if (!tis.config) return [];
79
+ return Object.entries(tis.config.methods).map(([id, m]) => ({
80
+ id,
81
+ label: (m as any)?.label ?? '',
82
+ }));
83
+ }, [tis.config]);
84
+
85
+ // Auto-select the default method on first successful load.
86
+ useEffect(() => {
87
+ if (!selectedId && tis.config) {
88
+ const fallback = tis.config.defaultMethodId
89
+ || Object.keys(tis.config.methods)[0]
90
+ || null;
91
+ setSelectedId(fallback);
92
+ }
93
+ }, [tis.config, selectedId]);
94
+
95
+ // Clear errors whenever the selected method changes.
96
+ useEffect(() => { setDraftError(null); }, [selectedId]);
97
+
98
+ const onApply = async (next: TestMethod) => {
99
+ if (!selectedId) return;
100
+ setBusy(true);
101
+ try {
102
+ await tis.putMethod(selectedId, next as any);
103
+ setDraftError(null);
104
+ } catch (e: any) {
105
+ setDraftError(String(e?.message ?? e));
106
+ } finally {
107
+ setBusy(false);
108
+ }
109
+ };
110
+
111
+ const onCreate = async () => {
112
+ const id = newId.trim();
113
+ if (!id) return;
114
+ if (tis.config?.methods[id]) {
115
+ setDraftError(`A method named "${id}" already exists.`);
116
+ return;
117
+ }
118
+ setBusy(true);
119
+ try {
120
+ await tis.putMethod(id, EMPTY_METHOD);
121
+ setSelectedId(id);
122
+ setNewDialogOpen(false);
123
+ setNewId('');
124
+ } catch (e: any) {
125
+ setDraftError(String(e?.message ?? e));
126
+ } finally {
127
+ setBusy(false);
128
+ }
129
+ };
130
+
131
+ const onDuplicate = async () => {
132
+ if (!selectedId || !tis.config) return;
133
+ const source = tis.config.methods[selectedId];
134
+ if (!source) return;
135
+ let candidate = `${selectedId}_copy`;
136
+ let n = 2;
137
+ while (tis.config.methods[candidate]) {
138
+ candidate = `${selectedId}_copy_${n++}`;
139
+ }
140
+ setBusy(true);
141
+ try {
142
+ await tis.putMethod(candidate, JSON.parse(JSON.stringify(source)));
143
+ setSelectedId(candidate);
144
+ } catch (e: any) {
145
+ setDraftError(String(e?.message ?? e));
146
+ } finally {
147
+ setBusy(false);
148
+ }
149
+ };
150
+
151
+ const onDelete = async () => {
152
+ if (!selectedId) return;
153
+ if (!window.confirm(`Remove method "${selectedId}"? This is staged — Save persists it.`)) return;
154
+ setBusy(true);
155
+ try {
156
+ await tis.removeMethod(selectedId);
157
+ setSelectedId(null);
158
+ } catch (e: any) {
159
+ setDraftError(String(e?.message ?? e));
160
+ } finally {
161
+ setBusy(false);
162
+ }
163
+ };
164
+
165
+ // Save flows through the diff dialog: it fetches the current disk
166
+ // state via tis.list_schemas, shows the operator what's about to land,
167
+ // and only invokes save_config on confirm.
168
+ const openSaveDialog = () => setSaveDialogOpen(true);
169
+ const onSaveConfirm = async () => {
170
+ setBusy(true);
171
+ try {
172
+ await tis.save();
173
+ setSaveDialogOpen(false);
174
+ } catch (e: any) {
175
+ setDraftError(String(e?.message ?? e));
176
+ } finally {
177
+ setBusy(false);
178
+ }
179
+ };
180
+
181
+ const onRevert = async () => {
182
+ if (!window.confirm('Discard all in-progress edits? This cannot be undone.')) return;
183
+ setBusy(true);
184
+ try {
185
+ await tis.revert();
186
+ } catch (e: any) {
187
+ setDraftError(String(e?.message ?? e));
188
+ } finally {
189
+ setBusy(false);
190
+ }
191
+ };
192
+
193
+ return (
194
+ <div className="tis-editor">
195
+ <header className="tis-editor__header">
196
+ <h2>
197
+ Test Methods{' '}
198
+ {tis.config?.dirty && (
199
+ <span className="tis-editor__dirty-pill">unsaved</span>
200
+ )}
201
+ </h2>
202
+ <div className="tis-editor__header-actions">
203
+ <Button
204
+ label="Save…"
205
+ icon="pi pi-save"
206
+ disabled={busy || !tis.config?.dirty}
207
+ onClick={openSaveDialog}
208
+ />
209
+ <Button
210
+ label="Revert"
211
+ icon="pi pi-undo"
212
+ className="p-button-secondary"
213
+ disabled={busy || !tis.config?.dirty}
214
+ onClick={onRevert}
215
+ />
216
+ </div>
217
+ </header>
218
+
219
+ {tis.error && (
220
+ <div className="tis-editor__error">
221
+ <strong>Error:</strong> <pre>{tis.error}</pre>
222
+ </div>
223
+ )}
224
+
225
+ <div className="tis-editor__body">
226
+ <aside className="tis-editor__sidebar">
227
+ <div className="tis-editor__sidebar-actions">
228
+ <Button
229
+ label="New"
230
+ icon="pi pi-plus"
231
+ disabled={busy}
232
+ onClick={() => setNewDialogOpen(true)}
233
+ />
234
+ <Button
235
+ label="Duplicate"
236
+ icon="pi pi-clone"
237
+ className="p-button-secondary"
238
+ disabled={busy || !selectedId}
239
+ onClick={onDuplicate}
240
+ />
241
+ <Button
242
+ label="Delete"
243
+ icon="pi pi-trash"
244
+ className="p-button-danger"
245
+ disabled={busy || !selectedId}
246
+ onClick={onDelete}
247
+ />
248
+ </div>
249
+ <DataTable
250
+ value={rows}
251
+ selection={rows.find(r => r.id === selectedId) ?? null}
252
+ onSelectionChange={(e) => setSelectedId((e.value as MethodRow | null)?.id ?? null)}
253
+ selectionMode="single"
254
+ dataKey="id"
255
+ scrollable
256
+ scrollHeight="flex"
257
+ emptyMessage={tis.loading ? 'Loading…' : 'No test methods defined.'}
258
+ >
259
+ <Column field="id" header="Method ID" />
260
+ <Column field="label" header="Label" />
261
+ </DataTable>
262
+ </aside>
263
+
264
+ <section className="tis-editor__detail">
265
+ {selectedId && tis.config?.methods[selectedId] ? (
266
+ <>
267
+ {draftError && (
268
+ <div className="tis-editor__error">
269
+ <pre>{draftError}</pre>
270
+ </div>
271
+ )}
272
+ <MethodFormEditor
273
+ methodId={selectedId}
274
+ method={tis.config.methods[selectedId] as TestMethod}
275
+ onApply={onApply}
276
+ busy={busy}
277
+ knownAssetTypes={ams.types}
278
+ />
279
+ </>
280
+ ) : (
281
+ <div className="tis-editor__empty">
282
+ Select a test method on the left, or create a new one.
283
+ </div>
284
+ )}
285
+ </section>
286
+ </div>
287
+
288
+ <Dialog
289
+ header="New Test Method"
290
+ visible={newDialogOpen}
291
+ onHide={() => setNewDialogOpen(false)}
292
+ style={{ width: '24rem' }}
293
+ >
294
+ <label className="tis-editor__new-method-label">
295
+ Method ID
296
+ <InputText
297
+ value={newId}
298
+ onChange={(e) => setNewId(e.target.value)}
299
+ placeholder="e.g. translational_traction"
300
+ autoFocus
301
+ />
302
+ </label>
303
+ <small>Canonical key — appears in wire payloads, on-disk paths, and generated code.</small>
304
+ <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
305
+ <Button label="Cancel" className="p-button-text" onClick={() => setNewDialogOpen(false)} />
306
+ <Button label="Create" disabled={!newId.trim() || busy} onClick={onCreate} />
307
+ </div>
308
+ </Dialog>
309
+
310
+ <SaveDiffDialog
311
+ visible={saveDialogOpen}
312
+ staged={(tis.config?.methods ?? {}) as Record<string, TestMethod>}
313
+ invoker={effectiveInvoker}
314
+ onConfirm={onSaveConfirm}
315
+ onCancel={() => setSaveDialogOpen(false)}
316
+ />
317
+ </div>
318
+ );
319
+ };
320
+
321
+ export default TisConfigEditor;
@@ -0,0 +1,54 @@
1
+ import { InputText } from 'primereact/inputtext';
2
+ import { Checkbox } from 'primereact/checkbox';
3
+ import { FormSection } from '../../forms/FormSection';
4
+ import { FormRow } from '../../forms/FormRow';
5
+ import type { AnalysisShape, TestMethod } from '../types';
6
+
7
+ export interface AnalysisEditorProps {
8
+ method: TestMethod;
9
+ onChange: (next: TestMethod) => void;
10
+ }
11
+
12
+ const empty = (): AnalysisShape => ({ script: '', function: '' });
13
+
14
+ export const AnalysisEditor: React.FC<AnalysisEditorProps> = ({ method, onChange }) => {
15
+ const a = method.analysis as AnalysisShape | null | undefined;
16
+ const enabled = !!a;
17
+
18
+ const setAnalysis = (next: AnalysisShape | null) => {
19
+ onChange({ ...method, analysis: next });
20
+ };
21
+
22
+ return (
23
+ <FormSection
24
+ title="Analysis Hook"
25
+ description="Optional post-cycle Python entry point. Codegen wires this into the method's TestManager."
26
+ actions={
27
+ <Checkbox
28
+ checked={enabled}
29
+ onChange={(e) => setAnalysis(e.checked ? empty() : null)}
30
+ />
31
+ }
32
+ >
33
+ {!enabled && <small>Analysis hook disabled. Tick the box to enable.</small>}
34
+ {enabled && a && (
35
+ <>
36
+ <FormRow label="Script" required hint="Path under autocore-python's scripts directory.">
37
+ <InputText
38
+ value={a.script}
39
+ onChange={(e) => setAnalysis({ ...a, script: e.target.value })}
40
+ placeholder="e.g. analysis/traction_v1.py"
41
+ />
42
+ </FormRow>
43
+ <FormRow label="Function" required hint="Entry-point name within the script.">
44
+ <InputText
45
+ value={a.function}
46
+ onChange={(e) => setAnalysis({ ...a, function: e.target.value })}
47
+ placeholder="e.g. analyze_cycle"
48
+ />
49
+ </FormRow>
50
+ </>
51
+ )}
52
+ </FormSection>
53
+ );
54
+ };
@@ -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
+ };