@adcops/autocore-react 3.5.15 → 3.6.3
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/tis-editor/DeleteMethodDialog.d.ts +43 -0
- package/dist/components/tis-editor/DeleteMethodDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/DeleteMethodDialog.js +1 -0
- package/dist/components/tis-editor/MethodTransfer.d.ts +69 -0
- package/dist/components/tis-editor/MethodTransfer.d.ts.map +1 -0
- package/dist/components/tis-editor/MethodTransfer.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +31 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts +9 -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/IdentitySection.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/IdentitySection.js +1 -1
- package/dist/components/tis-editor/types.d.ts +11 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/varpick/VariablePicker.d.ts.map +1 -1
- package/dist/components/varpick/VariablePicker.js +1 -1
- package/dist/components/varpick/varpick.css +84 -30
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useMethodSequence.d.ts +60 -0
- package/dist/hooks/useMethodSequence.d.ts.map +1 -0
- package/dist/hooks/useMethodSequence.js +1 -0
- package/package.json +1 -1
- package/src/components/tis-editor/DeleteMethodDialog.tsx +162 -0
- package/src/components/tis-editor/MethodTransfer.ts +138 -0
- package/src/components/tis-editor/TisConfigEditor.css +31 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +305 -7
- package/src/components/tis-editor/editor/IdentitySection.tsx +33 -1
- package/src/components/tis-editor/types.ts +11 -0
- package/src/components/varpick/VariablePicker.tsx +4 -0
- package/src/components/varpick/varpick.css +84 -30
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useMethodSequence.ts +128 -0
- package/tools/tests/dist/{TestFieldDialog-BvHAr2GQ.js → TestFieldDialog-Dph2oPfe.js} +1 -0
- package/tools/tests/dist/autocore-react.css +84 -30
- package/tools/tests/dist/tis-editor.entry.js +1 -1
- package/tools/tests/dist/varpick.entry.js +1 -1
- package/tools/tests/varpick.test.mjs +5 -0
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
* Left: PrimeReact DataTable listing methods (method_id + label).
|
|
5
5
|
* Right: tabbed editor for the selected method. Phase 1 ships a single
|
|
6
6
|
* JSON-via-Monaco tab; Phase 2 layers form editors on top of it.
|
|
7
|
-
* Action bar: New / Duplicate / Delete / Apply / Save / Revert.
|
|
7
|
+
* Action bar: New / Duplicate / Delete / Import / Export / Apply / Save / Revert.
|
|
8
|
+
*
|
|
9
|
+
* ## A method is two files
|
|
10
|
+
*
|
|
11
|
+
* A method whose `procedure` is `"sequence"` also owns
|
|
12
|
+
* `methods/<method_id>.seq.json`. This editor is the only place a method is
|
|
13
|
+
* created, copied, deleted or moved between machines, so it is the only place
|
|
14
|
+
* that can keep the two halves together — see `useMethodSequence` for what goes
|
|
15
|
+
* wrong when they drift. Every lifecycle action below therefore moves both.
|
|
8
16
|
*
|
|
9
17
|
* "Apply" pushes the local Monaco buffer to the server-side stage
|
|
10
18
|
* (`tis.put_method`). "Save" persists the entire stage to test_methods.json
|
|
@@ -15,12 +23,14 @@
|
|
|
15
23
|
* client gives clearer UX.
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
26
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
19
27
|
import { DataTable } from 'primereact/datatable';
|
|
20
28
|
import { Column } from 'primereact/column';
|
|
21
29
|
import { Button } from 'primereact/button';
|
|
22
30
|
import { InputText } from 'primereact/inputtext';
|
|
23
31
|
import { Dialog } from 'primereact/dialog';
|
|
32
|
+
import { Message } from 'primereact/message';
|
|
33
|
+
import { SelectButton } from 'primereact/selectbutton';
|
|
24
34
|
import { useContext } from 'react';
|
|
25
35
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
26
36
|
import { MessageType } from '../../hub/CommandMessage';
|
|
@@ -28,7 +38,13 @@ import { useTisConfig, type TisIpcInvoker } from '../../hooks/useTisConfig';
|
|
|
28
38
|
import { useAmsAssetTypes } from '../../hooks/useAmsAssetTypes';
|
|
29
39
|
import { MethodFormEditor } from './editor/MethodFormEditor';
|
|
30
40
|
import { SaveDiffDialog } from './editor/SaveDiffDialog';
|
|
41
|
+
import { DeleteMethodDialog } from './DeleteMethodDialog';
|
|
42
|
+
import {
|
|
43
|
+
buildTransfer, downloadTransfer, METHOD_ID_RE, METHOD_ID_RULE, parseTransfer,
|
|
44
|
+
type MethodTransferFile,
|
|
45
|
+
} from './MethodTransfer';
|
|
31
46
|
import type { TestMethod } from './types';
|
|
47
|
+
import { useMethodSequence, emptySequence } from '../../hooks/useMethodSequence';
|
|
32
48
|
|
|
33
49
|
import './TisConfigEditor.css';
|
|
34
50
|
import { useGmVariables } from '../../hooks/useGmVariables';
|
|
@@ -88,6 +104,28 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
88
104
|
// New-method dialog state.
|
|
89
105
|
const [newDialogOpen, setNewDialogOpen] = useState<boolean>(false);
|
|
90
106
|
const [newId, setNewId] = useState<string>('');
|
|
107
|
+
// A new method declares its procedure up front, because the choice decides
|
|
108
|
+
// whether creating it also scaffolds a sequence file. Defaulting to
|
|
109
|
+
// `sequence` is deliberate on a machine that has a sequencer: the other
|
|
110
|
+
// option cannot be made to work without a rebuild, so offering it as the
|
|
111
|
+
// default would be offering the operator a dead end.
|
|
112
|
+
const [newProcedure, setNewProcedure] = useState<'control' | 'sequence'>('sequence');
|
|
113
|
+
|
|
114
|
+
// Delete flow (archive + typed confirmation) — see DeleteMethodDialog.
|
|
115
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
|
|
116
|
+
|
|
117
|
+
// Import flow. `pendingImport` holds a parsed file waiting on the operator
|
|
118
|
+
// to resolve a name collision.
|
|
119
|
+
const fileInput = useRef<HTMLInputElement>(null);
|
|
120
|
+
const [pendingImport, setPendingImport] = useState<MethodTransferFile | null>(null);
|
|
121
|
+
const [importId, setImportId] = useState<string>('');
|
|
122
|
+
const [importError, setImportError] = useState<string | null>(null);
|
|
123
|
+
|
|
124
|
+
// The sequence half of a method. All method lifecycle actions move both.
|
|
125
|
+
// No `invoker` override: TisIpcInvoker speaks the `tis.*` request shape
|
|
126
|
+
// (topic, payload), and these are raw datastore reads and writes that need
|
|
127
|
+
// the message type as well.
|
|
128
|
+
const seqIo = useMethodSequence();
|
|
91
129
|
|
|
92
130
|
const rows: MethodRow[] = useMemo(() => {
|
|
93
131
|
if (!tis.config) return [];
|
|
@@ -135,13 +173,31 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
135
173
|
const onCreate = async () => {
|
|
136
174
|
const id = newId.trim();
|
|
137
175
|
if (!id) return;
|
|
176
|
+
// Validated here and not just in the dialog: the id becomes a path
|
|
177
|
+
// segment and a Rust identifier stem downstream, and neither of those
|
|
178
|
+
// failures is legible where they surface.
|
|
179
|
+
if (!METHOD_ID_RE.test(id)) {
|
|
180
|
+
setDraftError(`"${id}" is not a valid method id. ${METHOD_ID_RULE}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
138
183
|
if (tis.config?.methods[id]) {
|
|
139
184
|
setDraftError(`A method named "${id}" already exists.`);
|
|
140
185
|
return;
|
|
141
186
|
}
|
|
142
187
|
setBusy(true);
|
|
143
188
|
try {
|
|
144
|
-
|
|
189
|
+
const method: TestMethod = newProcedure === 'sequence'
|
|
190
|
+
? { ...EMPTY_METHOD, procedure: 'sequence' }
|
|
191
|
+
: { ...EMPTY_METHOD };
|
|
192
|
+
// Sequence FIRST. A method whose schema exists without its procedure
|
|
193
|
+
// refuses to start with "has no sequence on this machine"; the
|
|
194
|
+
// reverse leaves an unreferenced file that the next method of the
|
|
195
|
+
// same name would silently inherit. Of the two half-states, the
|
|
196
|
+
// orphan file is the one that cannot mislead a run.
|
|
197
|
+
if (newProcedure === 'sequence') {
|
|
198
|
+
await seqIo.write(id, emptySequence(id));
|
|
199
|
+
}
|
|
200
|
+
await tis.putMethod(id, method as any);
|
|
145
201
|
setSelectedId(id);
|
|
146
202
|
setNewDialogOpen(false);
|
|
147
203
|
setNewId('');
|
|
@@ -163,6 +219,10 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
163
219
|
}
|
|
164
220
|
setBusy(true);
|
|
165
221
|
try {
|
|
222
|
+
// The procedure comes with it. `seqIo.copy` rewrites the sequence's
|
|
223
|
+
// own `name` to the new id, so the copy does not claim to be its
|
|
224
|
+
// source — the method id owns the identity.
|
|
225
|
+
await seqIo.copy(selectedId, candidate);
|
|
166
226
|
await tis.putMethod(candidate, JSON.parse(JSON.stringify(source)));
|
|
167
227
|
setSelectedId(candidate);
|
|
168
228
|
} catch (e: any) {
|
|
@@ -172,13 +232,62 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
172
232
|
}
|
|
173
233
|
};
|
|
174
234
|
|
|
175
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Build and hand over an archive of every run recorded under `methodId`.
|
|
237
|
+
* Gates the delete confirmation; see DeleteMethodDialog.
|
|
238
|
+
*/
|
|
239
|
+
const downloadMethodArchive = async (methodId: string): Promise<boolean> => {
|
|
240
|
+
const resp: any = await effectiveInvoker('tis.export_method_zip', {
|
|
241
|
+
project_id: projectId,
|
|
242
|
+
method_id: methodId,
|
|
243
|
+
});
|
|
244
|
+
if (!resp?.success) {
|
|
245
|
+
throw new Error(resp?.error_message ?? 'export_method_zip failed');
|
|
246
|
+
}
|
|
247
|
+
// Archives ride the server's /downloads/ endpoint rather than the
|
|
248
|
+
// WebSocket frame: a method with a season of history is not a payload.
|
|
249
|
+
const url = typeof resp.data?.download_url === 'string' ? resp.data.download_url : '';
|
|
250
|
+
if (!url) throw new Error('the server returned no download URL');
|
|
251
|
+
const a = document.createElement('a');
|
|
252
|
+
a.href = url;
|
|
253
|
+
a.download = typeof resp.data?.filename === 'string' && resp.data.filename
|
|
254
|
+
? resp.data.filename
|
|
255
|
+
: `${methodId}_method_archive.zip`;
|
|
256
|
+
a.click();
|
|
257
|
+
return true;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const onDeleteConfirmed = async () => {
|
|
176
261
|
if (!selectedId) return;
|
|
177
|
-
if (!window.confirm(`Remove method "${selectedId}"? This is staged — Save persists it.`)) return;
|
|
178
262
|
setBusy(true);
|
|
179
263
|
try {
|
|
264
|
+
// Schema first here, unlike create: the schema is what makes the
|
|
265
|
+
// method selectable, so removing it first means an interrupted
|
|
266
|
+
// delete leaves an unreachable file rather than a method that is
|
|
267
|
+
// still offered and no longer runs.
|
|
180
268
|
await tis.removeMethod(selectedId);
|
|
269
|
+
await seqIo.remove(selectedId);
|
|
181
270
|
setSelectedId(null);
|
|
271
|
+
setDeleteDialogOpen(false);
|
|
272
|
+
} catch (e: any) {
|
|
273
|
+
setDraftError(String(e?.message ?? e));
|
|
274
|
+
} finally {
|
|
275
|
+
setBusy(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** The selected method and its procedure, as one portable file. */
|
|
280
|
+
const onExport = async () => {
|
|
281
|
+
if (!selectedId || !tis.config) return;
|
|
282
|
+
const schema = tis.config.methods[selectedId] as TestMethod | undefined;
|
|
283
|
+
if (!schema) return;
|
|
284
|
+
setBusy(true);
|
|
285
|
+
try {
|
|
286
|
+
// The STAGED schema, deliberately: what the operator sees on screen
|
|
287
|
+
// is what they mean to hand over. Exporting the on-disk copy instead
|
|
288
|
+
// would silently ship a version they had already changed.
|
|
289
|
+
const sequence = schema.procedure === 'sequence' ? await seqIo.read(selectedId) : null;
|
|
290
|
+
downloadTransfer(buildTransfer(selectedId, schema, sequence));
|
|
182
291
|
} catch (e: any) {
|
|
183
292
|
setDraftError(String(e?.message ?? e));
|
|
184
293
|
} finally {
|
|
@@ -186,6 +295,51 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
186
295
|
}
|
|
187
296
|
};
|
|
188
297
|
|
|
298
|
+
const onImportFile = async (file: File) => {
|
|
299
|
+
const result = parseTransfer(await file.text());
|
|
300
|
+
if ('error' in result) {
|
|
301
|
+
setImportError(`${file.name}: ${result.error}`);
|
|
302
|
+
setPendingImport(null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
setImportError(null);
|
|
306
|
+
setPendingImport(result.file);
|
|
307
|
+
setImportId(result.file.method_id);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Land a parsed import under `importId`.
|
|
312
|
+
*
|
|
313
|
+
* The dialog always asks, even when the name is free: an operator importing
|
|
314
|
+
* a colleague's method usually wants it called something of their own, and
|
|
315
|
+
* a list that fills up with other people's names is the complaint that
|
|
316
|
+
* follows. When the name IS taken, the same dialog is where they choose
|
|
317
|
+
* between replacing and renaming — the two are one decision, so they belong
|
|
318
|
+
* on one screen.
|
|
319
|
+
*/
|
|
320
|
+
const onImportConfirm = async () => {
|
|
321
|
+
if (!pendingImport) return;
|
|
322
|
+
const id = importId.trim();
|
|
323
|
+
if (!METHOD_ID_RE.test(id)) {
|
|
324
|
+
setImportError(`"${id}" is not a valid method id. ${METHOD_ID_RULE}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
setBusy(true);
|
|
328
|
+
try {
|
|
329
|
+
if (pendingImport.sequence) {
|
|
330
|
+
await seqIo.write(id, pendingImport.sequence);
|
|
331
|
+
}
|
|
332
|
+
await tis.putMethod(id, pendingImport.schema as any);
|
|
333
|
+
setSelectedId(id);
|
|
334
|
+
setPendingImport(null);
|
|
335
|
+
setImportError(null);
|
|
336
|
+
} catch (e: any) {
|
|
337
|
+
setImportError(String(e?.message ?? e));
|
|
338
|
+
} finally {
|
|
339
|
+
setBusy(false);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
189
343
|
// Save flows through the diff dialog: it fetches the current disk
|
|
190
344
|
// state via tis.list_schemas, shows the operator what's about to land,
|
|
191
345
|
// and only invokes save_config on confirm.
|
|
@@ -279,9 +433,47 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
279
433
|
icon="pi pi-trash"
|
|
280
434
|
className="p-button-danger"
|
|
281
435
|
disabled={busy || !selectedId}
|
|
282
|
-
onClick={
|
|
436
|
+
onClick={() => setDeleteDialogOpen(true)}
|
|
283
437
|
/>
|
|
284
438
|
</div>
|
|
439
|
+
{/* Import / Export move the WHOLE method — schema and
|
|
440
|
+
procedure in one file. Half a method looks like a method
|
|
441
|
+
and silently is not; see MethodTransfer. */}
|
|
442
|
+
<div className="tis-editor__sidebar-actions">
|
|
443
|
+
<Button
|
|
444
|
+
label="Import"
|
|
445
|
+
icon="pi pi-upload"
|
|
446
|
+
className="p-button-secondary"
|
|
447
|
+
disabled={busy}
|
|
448
|
+
onClick={() => fileInput.current?.click()}
|
|
449
|
+
/>
|
|
450
|
+
<Button
|
|
451
|
+
label="Export"
|
|
452
|
+
icon="pi pi-download"
|
|
453
|
+
className="p-button-secondary"
|
|
454
|
+
disabled={busy || !selectedId}
|
|
455
|
+
onClick={onExport}
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
{importError && (
|
|
459
|
+
<div className="tis-editor__error">
|
|
460
|
+
<pre>{importError}</pre>
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
{/* Hidden by design, not by CSS trickery: a styled Import
|
|
464
|
+
button is a far better target than the browser's native
|
|
465
|
+
file input on a touch panel. */}
|
|
466
|
+
<input
|
|
467
|
+
ref={fileInput}
|
|
468
|
+
type="file"
|
|
469
|
+
accept=".json,.method.json,application/json"
|
|
470
|
+
style={{ display: 'none' }}
|
|
471
|
+
onChange={(e) => {
|
|
472
|
+
const f = e.target.files?.[0];
|
|
473
|
+
e.target.value = '';
|
|
474
|
+
if (f) void onImportFile(f);
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
285
477
|
<DataTable
|
|
286
478
|
value={rows}
|
|
287
479
|
selection={rows.find(r => r.id === selectedId) ?? null}
|
|
@@ -337,12 +529,118 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
|
|
|
337
529
|
/>
|
|
338
530
|
</label>
|
|
339
531
|
<small>Canonical key — appears in wire payloads, on-disk paths, and generated code.</small>
|
|
532
|
+
{/* Stated on screen, not behind a tooltip: the rule is strict
|
|
533
|
+
because this id becomes a path segment and a Rust identifier
|
|
534
|
+
stem, and neither failure is legible where it surfaces. */}
|
|
535
|
+
{newId.trim() !== '' && !METHOD_ID_RE.test(newId.trim()) && (
|
|
536
|
+
<Message severity="warn" text={METHOD_ID_RULE} style={{ width: '100%', marginTop: '0.5rem' }} />
|
|
537
|
+
)}
|
|
538
|
+
|
|
539
|
+
<label className="tis-editor__new-method-label" style={{ marginTop: '1rem' }}>
|
|
540
|
+
Procedure
|
|
541
|
+
<SelectButton
|
|
542
|
+
value={newProcedure}
|
|
543
|
+
options={[
|
|
544
|
+
{ label: 'Sequence', value: 'sequence' },
|
|
545
|
+
{ label: 'Control program', value: 'control' },
|
|
546
|
+
]}
|
|
547
|
+
allowEmpty={false}
|
|
548
|
+
onChange={(e) => e.value && setNewProcedure(e.value)}
|
|
549
|
+
/>
|
|
550
|
+
</label>
|
|
551
|
+
<small>
|
|
552
|
+
{newProcedure === 'sequence'
|
|
553
|
+
? 'The steps are a file you edit on the Method screen. Creating this method also creates an empty sequence.'
|
|
554
|
+
: 'The steps are compiled into the control program. A new method of this kind needs a rebuild and redeploy before it will run.'}
|
|
555
|
+
</small>
|
|
556
|
+
|
|
340
557
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
|
|
341
558
|
<Button label="Cancel" className="p-button-text" onClick={() => setNewDialogOpen(false)} />
|
|
342
|
-
<Button
|
|
559
|
+
<Button
|
|
560
|
+
label="Create"
|
|
561
|
+
disabled={!METHOD_ID_RE.test(newId.trim()) || busy}
|
|
562
|
+
onClick={onCreate}
|
|
563
|
+
/>
|
|
343
564
|
</div>
|
|
344
565
|
</Dialog>
|
|
345
566
|
|
|
567
|
+
{/* Import: always asks for the name. See onImportConfirm. */}
|
|
568
|
+
<Dialog
|
|
569
|
+
header="Import Test Method"
|
|
570
|
+
visible={pendingImport !== null}
|
|
571
|
+
onHide={() => { setPendingImport(null); setImportError(null); }}
|
|
572
|
+
style={{ width: '28rem' }}
|
|
573
|
+
>
|
|
574
|
+
{pendingImport && (() => {
|
|
575
|
+
const taken = !!tis.config?.methods[importId.trim()];
|
|
576
|
+
const idOk = METHOD_ID_RE.test(importId.trim());
|
|
577
|
+
return (
|
|
578
|
+
<>
|
|
579
|
+
<p style={{ marginTop: 0 }}>
|
|
580
|
+
<strong>{pendingImport.method_id}</strong>
|
|
581
|
+
{pendingImport.schema.label ? ` — ${pendingImport.schema.label}` : ''}
|
|
582
|
+
<br />
|
|
583
|
+
<small>
|
|
584
|
+
{pendingImport.sequence
|
|
585
|
+
? 'Includes a sequence procedure.'
|
|
586
|
+
: 'Control-program method — no sequence in this file.'}
|
|
587
|
+
</small>
|
|
588
|
+
</p>
|
|
589
|
+
|
|
590
|
+
<label className="tis-editor__new-method-label">
|
|
591
|
+
Import as
|
|
592
|
+
<InputText
|
|
593
|
+
value={importId}
|
|
594
|
+
onChange={(e) => setImportId(e.target.value)}
|
|
595
|
+
autoFocus
|
|
596
|
+
/>
|
|
597
|
+
</label>
|
|
598
|
+
{!idOk && importId.trim() !== '' && (
|
|
599
|
+
<Message severity="warn" text={METHOD_ID_RULE} style={{ width: '100%' }} />
|
|
600
|
+
)}
|
|
601
|
+
{taken && idOk && (
|
|
602
|
+
<Message
|
|
603
|
+
severity="warn"
|
|
604
|
+
text={`"${importId.trim()}" already exists. Importing will REPLACE it, `
|
|
605
|
+
+ `including its sequence. Change the name above to keep both.`}
|
|
606
|
+
style={{ width: '100%' }}
|
|
607
|
+
/>
|
|
608
|
+
)}
|
|
609
|
+
{importError && (
|
|
610
|
+
<Message severity="error" text={importError} style={{ width: '100%' }} />
|
|
611
|
+
)}
|
|
612
|
+
|
|
613
|
+
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
|
|
614
|
+
<Button
|
|
615
|
+
label="Cancel"
|
|
616
|
+
className="p-button-text"
|
|
617
|
+
onClick={() => { setPendingImport(null); setImportError(null); }}
|
|
618
|
+
/>
|
|
619
|
+
<Button
|
|
620
|
+
label={taken ? 'Replace' : 'Import'}
|
|
621
|
+
className={taken ? 'p-button-danger' : undefined}
|
|
622
|
+
disabled={!idOk || busy}
|
|
623
|
+
onClick={onImportConfirm}
|
|
624
|
+
/>
|
|
625
|
+
</div>
|
|
626
|
+
</>
|
|
627
|
+
);
|
|
628
|
+
})()}
|
|
629
|
+
</Dialog>
|
|
630
|
+
|
|
631
|
+
<DeleteMethodDialog
|
|
632
|
+
visible={deleteDialogOpen}
|
|
633
|
+
methodId={selectedId}
|
|
634
|
+
hasSequence={
|
|
635
|
+
!!selectedId
|
|
636
|
+
&& (tis.config?.methods[selectedId] as TestMethod | undefined)?.procedure === 'sequence'
|
|
637
|
+
}
|
|
638
|
+
onDownloadArchive={downloadMethodArchive}
|
|
639
|
+
onConfirm={onDeleteConfirmed}
|
|
640
|
+
onCancel={() => setDeleteDialogOpen(false)}
|
|
641
|
+
busy={busy}
|
|
642
|
+
/>
|
|
643
|
+
|
|
346
644
|
<SaveDiffDialog
|
|
347
645
|
visible={saveDialogOpen}
|
|
348
646
|
staged={(tis.config?.methods ?? {}) as Record<string, TestMethod>}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InputText } from 'primereact/inputtext';
|
|
2
2
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
3
|
+
import { SelectButton } from 'primereact/selectbutton';
|
|
3
4
|
import { FormSection } from '../../forms/FormSection';
|
|
4
5
|
import { FormRow } from '../../forms/FormRow';
|
|
5
6
|
import type { TestMethod } from '../types';
|
|
@@ -13,7 +14,7 @@ export const IdentitySection: React.FC<IdentitySectionProps> = ({ method, onChan
|
|
|
13
14
|
return (
|
|
14
15
|
<FormSection
|
|
15
16
|
title="Identity"
|
|
16
|
-
description="Display label
|
|
17
|
+
description="Display label, operator-facing description, and which engine runs the steps."
|
|
17
18
|
>
|
|
18
19
|
<FormRow label="Label" hint="Pretty name shown in the Test Method picker.">
|
|
19
20
|
<InputText
|
|
@@ -31,6 +32,37 @@ export const IdentitySection: React.FC<IdentitySectionProps> = ({ method, onChan
|
|
|
31
32
|
onChange={(e) => onChange({ ...method, description: e.target.value })}
|
|
32
33
|
/>
|
|
33
34
|
</FormRow>
|
|
35
|
+
{/* Which engine owns the steps.
|
|
36
|
+
`Sequence` is the one that can be authored on the machine: the
|
|
37
|
+
procedure is a file the sequence workbench edits, so a new
|
|
38
|
+
method runs without regenerating and rebuilding the control
|
|
39
|
+
program. `Control program` means compiled Rust, which needs a
|
|
40
|
+
rebuild for every method — the default only because it is what
|
|
41
|
+
every method predating this field does.
|
|
42
|
+
Absent rather than "control" when control is chosen, so a
|
|
43
|
+
method that never had an opinion keeps a clean record. */}
|
|
44
|
+
<FormRow
|
|
45
|
+
label="Procedure"
|
|
46
|
+
hint="Sequence: the steps are a file you edit on the Method screen. Control program: compiled Rust, requires a rebuild to add or change a method."
|
|
47
|
+
>
|
|
48
|
+
<SelectButton
|
|
49
|
+
value={(method.procedure as string) ?? 'control'}
|
|
50
|
+
options={[
|
|
51
|
+
{ label: 'Control program', value: 'control' },
|
|
52
|
+
{ label: 'Sequence', value: 'sequence' },
|
|
53
|
+
]}
|
|
54
|
+
allowEmpty={false}
|
|
55
|
+
onChange={(e) => {
|
|
56
|
+
const next: TestMethod = { ...method };
|
|
57
|
+
if (e.value === 'sequence') {
|
|
58
|
+
next.procedure = 'sequence';
|
|
59
|
+
} else {
|
|
60
|
+
delete next.procedure;
|
|
61
|
+
}
|
|
62
|
+
onChange(next);
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
</FormRow>
|
|
34
66
|
</FormSection>
|
|
35
67
|
);
|
|
36
68
|
};
|
|
@@ -188,6 +188,17 @@ export interface TestMethod {
|
|
|
188
188
|
asset_refs?: AssetRef[];
|
|
189
189
|
analysis?: AnalysisShape | null;
|
|
190
190
|
configurations?: TestConfiguration[];
|
|
191
|
+
/**
|
|
192
|
+
* Which engine runs this method's steps.
|
|
193
|
+
*
|
|
194
|
+
* `"sequence"` means the procedure is data at
|
|
195
|
+
* `methods/<method_id>.seq.json`, authored in the sequence workbench and
|
|
196
|
+
* executed by autocore-seq — so a method can be created on the machine and
|
|
197
|
+
* run without regenerating and rebuilding the control program.
|
|
198
|
+
* `"control"` (the default) means compiled Rust, which is every method that
|
|
199
|
+
* predates this field.
|
|
200
|
+
*/
|
|
201
|
+
procedure?: 'control' | 'sequence';
|
|
191
202
|
[key: string]: unknown; // tolerate unknown server-side fields
|
|
192
203
|
}
|
|
193
204
|
|
|
@@ -98,6 +98,10 @@ export const VariablePicker: React.FC<VariablePickerProps> = ({
|
|
|
98
98
|
visible={visible}
|
|
99
99
|
onHide={onCancel}
|
|
100
100
|
footer={footer}
|
|
101
|
+
// Carries the token bridge. PrimeReact portals the dialog to
|
|
102
|
+
// <body>, so it is not inside the field's subtree and cannot
|
|
103
|
+
// inherit the tokens declared there — see varpick.css.
|
|
104
|
+
className="ac-varpick"
|
|
101
105
|
style={{ width: '44rem', maxWidth: '95vw' }}
|
|
102
106
|
>
|
|
103
107
|
{hint && <div className="ac-varpick__hint">{hint}</div>}
|