@adcops/autocore-react 3.3.67 → 3.3.70
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/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +32 -0
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +25 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +17 -6
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +1 -1
- package/src/components/tis/TestDataView.tsx +37 -15
- package/src/components/tis/TestSetupForm.tsx +252 -19
- package/src/components/tis/TisProvider.tsx +50 -1
- package/src/themes/adc-dark/_extensions.scss +18 -6
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { Button } from 'primereact/button';
|
|
3
3
|
import { InputText } from 'primereact/inputtext';
|
|
4
|
-
import {
|
|
4
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
5
|
+
import { Dialog } from 'primereact/dialog';
|
|
5
6
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
7
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
7
8
|
import { MessageType } from '../../hub/CommandMessage';
|
|
8
9
|
import { ValueInput } from '../ValueInput';
|
|
9
10
|
import { TextInput } from '../TextInput';
|
|
10
11
|
import { useTis } from './TisProvider';
|
|
12
|
+
import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
|
|
11
13
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* One asset_ref declared on a test method. We only consume the subset
|
|
17
|
+
* the form cares about — `field`, `asset_type`, `select`, `from`,
|
|
18
|
+
* `location`. Other fields (calibration_required, label, description)
|
|
19
|
+
* exist on the wire but aren't form-relevant.
|
|
20
|
+
*/
|
|
21
|
+
interface TisAssetRef {
|
|
22
|
+
field: string;
|
|
23
|
+
asset_type: string;
|
|
24
|
+
select: 'by_location' | 'by_id_field';
|
|
25
|
+
from?: string;
|
|
26
|
+
location?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export interface TestFieldDef {
|
|
14
30
|
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
15
31
|
name: string;
|
|
@@ -29,12 +45,30 @@ export interface TestMethod {
|
|
|
29
45
|
config_fields: TestFieldDef[];
|
|
30
46
|
cycle_fields: TestFieldDef[];
|
|
31
47
|
results_fields: TestFieldDef[];
|
|
48
|
+
/**
|
|
49
|
+
* AMS asset references resolved at start_test. The form scans these
|
|
50
|
+
* for `select=by_id_field` entries pointing at a config field
|
|
51
|
+
* (`from: "config.<name>"`) and renders that field as a Dropdown of
|
|
52
|
+
* matching AMS assets instead of a free-form text input. No
|
|
53
|
+
* project.json change required — the form derives this from the
|
|
54
|
+
* existing schema.
|
|
55
|
+
*/
|
|
56
|
+
asset_refs?: TisAssetRef[];
|
|
32
57
|
/** Optional pretty label for the Test Method picker. Falls back
|
|
33
58
|
* to the canonical method_id key. */
|
|
34
59
|
label?: string;
|
|
35
60
|
/** Optional long-form description shown in the picker dialog
|
|
36
61
|
* when this method is highlighted. */
|
|
37
62
|
description?: string;
|
|
63
|
+
/** Optional post-cycle analysis dispatch (`{ script, function }`).
|
|
64
|
+
* Consumed server-side by the codegen; the form doesn't render
|
|
65
|
+
* anything based on it but accepts it so generated schema
|
|
66
|
+
* literals from `acctl codegen-tags` typecheck cleanly. */
|
|
67
|
+
analysis?: any;
|
|
68
|
+
/** Free-form view declarations for chart components. Same rationale
|
|
69
|
+
* as `analysis` — the form ignores them; the type just has to
|
|
70
|
+
* accept them so generated schemas typecheck. */
|
|
71
|
+
views?: Record<string, any>;
|
|
38
72
|
}
|
|
39
73
|
|
|
40
74
|
/**
|
|
@@ -69,6 +103,60 @@ const hasDescription = (f: TestFieldDef): boolean =>
|
|
|
69
103
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
70
104
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
71
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Dropdown over the active subset of AMS assets of a given type. Used
|
|
108
|
+
* for config fields that an `asset_ref` resolves via
|
|
109
|
+
* `select=by_id_field` — the operator picks an asset by ID from the
|
|
110
|
+
* registry rather than typing it. The dropdown's option labels
|
|
111
|
+
* include the asset's role (when the asset has a known role) and
|
|
112
|
+
* serial so the operator can identify "the right one" without leaving
|
|
113
|
+
* the form.
|
|
114
|
+
*
|
|
115
|
+
* Falls back to a graceful empty state when the AMS provider isn't
|
|
116
|
+
* mounted (no assets) — reads as "No matching assets registered" so
|
|
117
|
+
* the operator knows the gap and can go register one in Settings.
|
|
118
|
+
*/
|
|
119
|
+
const AssetIdPicker: React.FC<{
|
|
120
|
+
assetType: string;
|
|
121
|
+
value: string;
|
|
122
|
+
onChange: (val: string) => void;
|
|
123
|
+
invalid?: boolean;
|
|
124
|
+
}> = ({ assetType, value, onChange, invalid }) => {
|
|
125
|
+
const assets = useAmsAssets();
|
|
126
|
+
const roles = useAmsRoles();
|
|
127
|
+
|
|
128
|
+
const options = useMemo(() => {
|
|
129
|
+
const filtered = (assets as AmsAssetEntry[])
|
|
130
|
+
.filter(a => a.asset_type === assetType && a.status === 'active');
|
|
131
|
+
return filtered.map(a => {
|
|
132
|
+
const roleLabel = roles[a.asset_type]
|
|
133
|
+
?.find(r => r.location === a.location)?.label;
|
|
134
|
+
const labelParts = [a.asset_id];
|
|
135
|
+
if (roleLabel) labelParts.push(`— ${roleLabel}`);
|
|
136
|
+
else if (a.location) labelParts.push(`— ${a.location}`);
|
|
137
|
+
if (a.serial) labelParts.push(`(s/n ${a.serial})`);
|
|
138
|
+
return { label: labelParts.join(' '), value: a.asset_id };
|
|
139
|
+
});
|
|
140
|
+
}, [assets, roles, assetType]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Dropdown
|
|
144
|
+
value={value}
|
|
145
|
+
options={options}
|
|
146
|
+
onChange={(e) => onChange(e.value ?? '')}
|
|
147
|
+
placeholder={
|
|
148
|
+
options.length === 0
|
|
149
|
+
? `No active ${assetType} assets registered — add one in Settings → Assets`
|
|
150
|
+
: `Select ${assetType}…`
|
|
151
|
+
}
|
|
152
|
+
className={invalid ? 'p-invalid' : ''}
|
|
153
|
+
filter
|
|
154
|
+
showClear
|
|
155
|
+
disabled={options.length === 0}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
72
160
|
// -------------------------------------------------------------------------
|
|
73
161
|
|
|
74
162
|
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
@@ -92,8 +180,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
92
180
|
const [methodId, setMethodIdLocal] = useState<string>(
|
|
93
181
|
tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
|
|
94
182
|
);
|
|
95
|
-
|
|
96
|
-
|
|
183
|
+
// Sample ID + config_field values come from the TisProvider rather
|
|
184
|
+
// than local React state so they survive form unmount when the
|
|
185
|
+
// operator switches tabs. Initial value reads from selection /
|
|
186
|
+
// stagedConfig; subsequent edits write back to the provider.
|
|
187
|
+
const [sampleId, setSampleIdLocal] = useState<string>(tis.selection.sampleId || '');
|
|
188
|
+
const config = tis.stagedConfig;
|
|
189
|
+
const setConfig = (updater: any) => {
|
|
190
|
+
const next = typeof updater === 'function' ? updater(tis.stagedConfig) : updater;
|
|
191
|
+
if (next !== tis.stagedConfig) {
|
|
192
|
+
tis.setStagedConfig(next);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
97
195
|
|
|
98
196
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
99
197
|
|
|
@@ -132,6 +230,16 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
132
230
|
const [isValid, setIsValid] = useState(false);
|
|
133
231
|
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
134
232
|
|
|
233
|
+
// Single shared "info" dialog used by:
|
|
234
|
+
// - the Test Setup status button (shows what's wrong, including
|
|
235
|
+
// server-side last_start_error)
|
|
236
|
+
// - the per-field info buttons (touch-friendly replacement for
|
|
237
|
+
// the old hover-tooltip)
|
|
238
|
+
const [infoDialog, setInfoDialog] = useState<{ open: boolean; title: string; body: React.ReactNode }>(
|
|
239
|
+
{ open: false, title: '', body: null },
|
|
240
|
+
);
|
|
241
|
+
const closeInfoDialog = () => setInfoDialog({ open: false, title: '', body: null });
|
|
242
|
+
|
|
135
243
|
// Seed and live-update config_fields that declare a `source`.
|
|
136
244
|
useEffect(() => {
|
|
137
245
|
if (!schema) return;
|
|
@@ -218,15 +326,38 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
218
326
|
}
|
|
219
327
|
};
|
|
220
328
|
|
|
329
|
+
/**
|
|
330
|
+
* If the test method has an `asset_ref` whose `select=by_id_field`
|
|
331
|
+
* pulls from this config field, return the asset_type the field
|
|
332
|
+
* should pick from. The form renders that field as a dropdown of
|
|
333
|
+
* AMS assets instead of a free-form text input.
|
|
334
|
+
*
|
|
335
|
+
* The schema already encodes the relationship via `from:
|
|
336
|
+
* "config.<field_name>"` — no project.json change required.
|
|
337
|
+
*/
|
|
338
|
+
const assetTypeForField = (field: TestFieldDef): string | null => {
|
|
339
|
+
const refs = (schema?.asset_refs ?? []) as TisAssetRef[];
|
|
340
|
+
const expectedFrom = `config.${field.name}`;
|
|
341
|
+
const hit = refs.find(r => r.select === 'by_id_field' && r.from === expectedFrom);
|
|
342
|
+
return hit ? hit.asset_type : null;
|
|
343
|
+
};
|
|
344
|
+
|
|
221
345
|
const renderConfigField = (field: TestFieldDef) => {
|
|
222
346
|
if (field.name === 'sample_id') return null;
|
|
223
347
|
const valid = isFieldValid(field);
|
|
224
|
-
const
|
|
225
|
-
const
|
|
348
|
+
const assetType = assetTypeForField(field);
|
|
349
|
+
const isNum = !assetType && field.type !== 'string' && field.type !== 'bool';
|
|
226
350
|
return (
|
|
227
351
|
<React.Fragment key={field.name}>
|
|
228
352
|
<span className="ac-form-label">{labelOf(field)}</span>
|
|
229
|
-
{
|
|
353
|
+
{assetType ? (
|
|
354
|
+
<AssetIdPicker
|
|
355
|
+
assetType={assetType}
|
|
356
|
+
value={config[field.name] != null ? String(config[field.name]) : ''}
|
|
357
|
+
onChange={(val) => handleFieldChange(field, val)}
|
|
358
|
+
invalid={!valid}
|
|
359
|
+
/>
|
|
360
|
+
) : isNum ? (
|
|
230
361
|
<ValueInput
|
|
231
362
|
label={undefined}
|
|
232
363
|
value={config[field.name] != null ? Number(config[field.name]) : null}
|
|
@@ -242,16 +373,19 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
242
373
|
/>
|
|
243
374
|
)}
|
|
244
375
|
{hasDescription(field) ? (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
376
|
+
<Button
|
|
377
|
+
icon="pi pi-info-circle"
|
|
378
|
+
text
|
|
379
|
+
rounded
|
|
380
|
+
aria-label={`About ${labelOf(field)}`}
|
|
381
|
+
onClick={() => setInfoDialog({
|
|
382
|
+
open: true,
|
|
383
|
+
title: labelOf(field),
|
|
384
|
+
body: <p style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{field.description}</p>,
|
|
385
|
+
})}
|
|
386
|
+
// Replaces a hover Tooltip — touch-friendly. The
|
|
387
|
+
// description is shown in the shared info dialog.
|
|
388
|
+
/>
|
|
255
389
|
) : (
|
|
256
390
|
<span aria-hidden="true" />
|
|
257
391
|
)}
|
|
@@ -295,13 +429,97 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
295
429
|
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
296
430
|
};
|
|
297
431
|
|
|
432
|
+
// Build the list of reasons the form is invalid. Used by the
|
|
433
|
+
// status button to show actionable diagnostics, not just a red icon.
|
|
434
|
+
const validationIssues = (): string[] => {
|
|
435
|
+
const issues: string[] = [];
|
|
436
|
+
if (!projectExists) {
|
|
437
|
+
issues.push('No project is selected. Pick or create one on the Project tab.');
|
|
438
|
+
}
|
|
439
|
+
if (!tis.projectFieldsLoaded) {
|
|
440
|
+
issues.push('Project fields are still loading.');
|
|
441
|
+
}
|
|
442
|
+
if (!methodId.trim()) {
|
|
443
|
+
issues.push('No test method selected.');
|
|
444
|
+
}
|
|
445
|
+
if (!sampleId.trim()) {
|
|
446
|
+
issues.push('Sample ID is required.');
|
|
447
|
+
}
|
|
448
|
+
if (schema) {
|
|
449
|
+
for (const field of schema.config_fields) {
|
|
450
|
+
if (field.name === 'sample_id') continue;
|
|
451
|
+
if (!field.required) continue;
|
|
452
|
+
const v = config[field.name];
|
|
453
|
+
if (v === undefined || v === '' || v === null) {
|
|
454
|
+
issues.push(`Required field "${labelOf(field)}" is empty.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return issues;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const openStatusDialog = () => {
|
|
462
|
+
const issues = validationIssues();
|
|
463
|
+
const serverErr = tis.state.lastStartError?.trim() ?? '';
|
|
464
|
+
let body: React.ReactNode;
|
|
465
|
+
if (issues.length === 0 && !serverErr) {
|
|
466
|
+
body = (
|
|
467
|
+
<p style={{ margin: 0 }}>
|
|
468
|
+
All required fields are complete. The test is staged and ready to start.
|
|
469
|
+
</p>
|
|
470
|
+
);
|
|
471
|
+
} else {
|
|
472
|
+
body = (
|
|
473
|
+
<div>
|
|
474
|
+
{issues.length > 0 && (
|
|
475
|
+
<>
|
|
476
|
+
<p style={{ margin: '0 0 0.5rem 0' }}>
|
|
477
|
+
The form is incomplete:
|
|
478
|
+
</p>
|
|
479
|
+
<ul style={{ margin: '0 0 1rem 1.25rem' }}>
|
|
480
|
+
{issues.map((m, i) => <li key={i}>{m}</li>)}
|
|
481
|
+
</ul>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
{serverErr && (
|
|
485
|
+
<>
|
|
486
|
+
<p style={{ margin: '0 0 0.25rem 0', fontWeight: 600 }}>
|
|
487
|
+
Last start_test error from the server:
|
|
488
|
+
</p>
|
|
489
|
+
<pre style={{
|
|
490
|
+
margin: 0,
|
|
491
|
+
padding: '0.5rem',
|
|
492
|
+
background: 'rgba(0,0,0,0.25)',
|
|
493
|
+
borderRadius: '4px',
|
|
494
|
+
whiteSpace: 'pre-wrap',
|
|
495
|
+
fontSize: '0.875rem',
|
|
496
|
+
}}>{serverErr}</pre>
|
|
497
|
+
</>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
setInfoDialog({ open: true, title: 'Test Setup Status', body });
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// The icon doubles as a button so the operator can drill into the
|
|
506
|
+
// *reason* the form isn't ready, including server-side errors that
|
|
507
|
+
// don't have a corresponding red field (most importantly, AMS
|
|
508
|
+
// resolution failures from the last Start attempt).
|
|
509
|
+
const statusValid = isValid && !tis.state.lastStartError;
|
|
510
|
+
|
|
298
511
|
return (
|
|
299
512
|
<div className="ac-form-grid" style={gridStyle}>
|
|
300
513
|
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
301
514
|
Test Setup
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
515
|
+
<Button
|
|
516
|
+
icon={statusValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'}
|
|
517
|
+
severity={statusValid ? 'success' : 'danger'}
|
|
518
|
+
text
|
|
519
|
+
rounded
|
|
520
|
+
aria-label="Test Setup status"
|
|
521
|
+
onClick={openStatusDialog}
|
|
522
|
+
/>
|
|
305
523
|
<span style={{
|
|
306
524
|
fontSize: '0.85em',
|
|
307
525
|
color: 'var(--text-secondary-color)',
|
|
@@ -360,6 +578,21 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
360
578
|
currentMethodId={methodId}
|
|
361
579
|
onSelected={(picked) => setMethodIdLocal(picked)}
|
|
362
580
|
/>
|
|
581
|
+
|
|
582
|
+
{/* Shared info dialog. Driven by the Test Setup status
|
|
583
|
+
button (validation + server errors) and by per-field
|
|
584
|
+
info buttons (replaces the old hover Tooltip). One
|
|
585
|
+
Dialog instance keeps the surface stack shallow. */}
|
|
586
|
+
<Dialog
|
|
587
|
+
header={infoDialog.title}
|
|
588
|
+
visible={infoDialog.open}
|
|
589
|
+
onHide={closeInfoDialog}
|
|
590
|
+
style={{ width: '32rem', maxWidth: '90vw' }}
|
|
591
|
+
modal
|
|
592
|
+
dismissableMask
|
|
593
|
+
>
|
|
594
|
+
{infoDialog.body}
|
|
595
|
+
</Dialog>
|
|
363
596
|
</div>
|
|
364
597
|
);
|
|
365
598
|
};
|
|
@@ -55,6 +55,17 @@ export interface TisLiveState {
|
|
|
55
55
|
activeMethodId: string;
|
|
56
56
|
activeSampleId: string;
|
|
57
57
|
activeRunId: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Most recent `tis.start_test` failure message broadcast by the
|
|
61
|
+
* server, or empty string when the last start succeeded (or none
|
|
62
|
+
* has been attempted). Used by `<TestSetupForm>` to surface AMS
|
|
63
|
+
* resolution errors and other server-side rejections inline,
|
|
64
|
+
* instead of leaving the operator staring at a non-responsive
|
|
65
|
+
* Start button. Cleared by the server on the next successful
|
|
66
|
+
* start_test.
|
|
67
|
+
*/
|
|
68
|
+
lastStartError: string;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export interface TisSelection {
|
|
@@ -129,6 +140,22 @@ export interface TisContextValue {
|
|
|
129
140
|
* by the create / edit dialogs after a successful submit. */
|
|
130
141
|
setProjectFields: (id: string, fields: Record<string, any>) => void;
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* In-progress test draft — the operator's pending Sample ID and
|
|
145
|
+
* config_field values for the active method. Held on the provider
|
|
146
|
+
* (not in `<TestSetupForm>`'s local React state) so the values
|
|
147
|
+
* survive remount when the operator switches tabs. Wiped via
|
|
148
|
+
* `clearStagedConfig()` after a successful start_test or when the
|
|
149
|
+
* operator cancels the staged record.
|
|
150
|
+
*/
|
|
151
|
+
stagedConfig: Record<string, any>;
|
|
152
|
+
/** Merge a partial patch into `stagedConfig`. Existing fields not
|
|
153
|
+
* mentioned in the patch are preserved. */
|
|
154
|
+
setStagedConfig: (patch: Record<string, any>) => void;
|
|
155
|
+
/** Reset `stagedConfig` to `{}`. Called by the form on a fresh
|
|
156
|
+
* method selection or after a run completes. */
|
|
157
|
+
clearStagedConfig: () => void;
|
|
158
|
+
|
|
132
159
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
133
160
|
* omitted to aggregate runs across every method in the project —
|
|
134
161
|
* the History tab uses this. */
|
|
@@ -146,6 +173,7 @@ export interface TisContextValue {
|
|
|
146
173
|
const EMPTY_STATE: TisLiveState = {
|
|
147
174
|
staged: false, stagedProjectId: '', stagedMethodId: '', stagedSampleId: '',
|
|
148
175
|
active: false, activeProjectId: '', activeMethodId: '', activeSampleId: '', activeRunId: '',
|
|
176
|
+
lastStartError: '',
|
|
149
177
|
};
|
|
150
178
|
|
|
151
179
|
const EMPTY_SELECTION: TisSelection = {
|
|
@@ -167,6 +195,9 @@ const TisContext = createContext<TisContextValue>({
|
|
|
167
195
|
projectFieldsLoaded: false,
|
|
168
196
|
loadProjectFields: async () => null,
|
|
169
197
|
setProjectFields: () => {},
|
|
198
|
+
stagedConfig: {},
|
|
199
|
+
setStagedConfig: () => {},
|
|
200
|
+
clearStagedConfig: () => {},
|
|
170
201
|
fetchRuns: async () => [],
|
|
171
202
|
fetchRun: async () => null,
|
|
172
203
|
runCache: {},
|
|
@@ -185,7 +216,8 @@ type StateAction =
|
|
|
185
216
|
| { kind: 'active_project_id'; value: string }
|
|
186
217
|
| { kind: 'active_method_id'; value: string }
|
|
187
218
|
| { kind: 'active_sample_id'; value: string }
|
|
188
|
-
| { kind: 'active_run_id'; value: string }
|
|
219
|
+
| { kind: 'active_run_id'; value: string }
|
|
220
|
+
| { kind: 'last_start_error'; value: string };
|
|
189
221
|
|
|
190
222
|
function liveReducer(s: TisLiveState, a: StateAction): TisLiveState {
|
|
191
223
|
switch (a.kind) {
|
|
@@ -198,6 +230,7 @@ function liveReducer(s: TisLiveState, a: StateAction): TisLiveState {
|
|
|
198
230
|
case 'active_method_id': return { ...s, activeMethodId: a.value };
|
|
199
231
|
case 'active_sample_id': return { ...s, activeSampleId: a.value };
|
|
200
232
|
case 'active_run_id': return { ...s, activeRunId: a.value };
|
|
233
|
+
case 'last_start_error': return { ...s, lastStartError: a.value };
|
|
201
234
|
}
|
|
202
235
|
}
|
|
203
236
|
|
|
@@ -279,6 +312,7 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
279
312
|
subscribe('tis.active_method_id', (v: any) => dispatch({ kind: 'active_method_id', value: String(v ?? '') })),
|
|
280
313
|
subscribe('tis.active_sample_id', (v: any) => dispatch({ kind: 'active_sample_id', value: String(v ?? '') })),
|
|
281
314
|
subscribe('tis.active_run_id', (v: any) => dispatch({ kind: 'active_run_id', value: String(v ?? '') })),
|
|
315
|
+
subscribe('tis.last_start_error', (v: any) => dispatch({ kind: 'last_start_error', value: String(v ?? '') })),
|
|
282
316
|
];
|
|
283
317
|
return () => { subs.forEach(unsubscribe); };
|
|
284
318
|
}, [subscribe, unsubscribe]);
|
|
@@ -506,17 +540,32 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
506
540
|
const projectFields = projectFieldsCache[selection.projectId] ?? {};
|
|
507
541
|
const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
|
|
508
542
|
|
|
543
|
+
// Operator's pending Sample ID and config_field values live here
|
|
544
|
+
// (rather than as local state in <TestSetupForm>) so they survive
|
|
545
|
+
// the form unmounting when the user switches to another tab. The
|
|
546
|
+
// form re-hydrates from this on remount, so leaving the Test tab
|
|
547
|
+
// and coming back is transparent.
|
|
548
|
+
const [stagedConfig, setStagedConfigState] = useState<Record<string, any>>({});
|
|
549
|
+
const setStagedConfig = useCallback((patch: Record<string, any>) => {
|
|
550
|
+
setStagedConfigState(prev => ({ ...prev, ...patch }));
|
|
551
|
+
}, []);
|
|
552
|
+
const clearStagedConfig = useCallback(() => {
|
|
553
|
+
setStagedConfigState({});
|
|
554
|
+
}, []);
|
|
555
|
+
|
|
509
556
|
const value: TisContextValue = useMemo(() => ({
|
|
510
557
|
schemas, defaultMethodId, schemasLoaded,
|
|
511
558
|
state, selection, setSelection,
|
|
512
559
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
513
560
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
561
|
+
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
514
562
|
fetchRuns, fetchRun, runCache,
|
|
515
563
|
}), [
|
|
516
564
|
schemas, defaultMethodId, schemasLoaded,
|
|
517
565
|
state, selection, setSelection,
|
|
518
566
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
519
567
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
568
|
+
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
520
569
|
fetchRuns, fetchRun, runCache,
|
|
521
570
|
]);
|
|
522
571
|
|
|
@@ -156,8 +156,8 @@
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
.ac-toolbar-icon-btn {
|
|
159
|
-
width:
|
|
160
|
-
height:
|
|
159
|
+
width: 19mm;
|
|
160
|
+
height: 12.7mm;
|
|
161
161
|
padding: 0;
|
|
162
162
|
display: inline-flex;
|
|
163
163
|
align-items: center;
|
|
@@ -170,8 +170,8 @@
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
.ac-toolbar-icon-lg {
|
|
173
|
-
width:
|
|
174
|
-
height:
|
|
173
|
+
width: 25.4mm;
|
|
174
|
+
height: 19mm;
|
|
175
175
|
padding: 0;
|
|
176
176
|
display: inline-flex;
|
|
177
177
|
align-items: center;
|
|
@@ -184,8 +184,8 @@
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
.ac-toolbar-icon-panic {
|
|
187
|
-
width:
|
|
188
|
-
height:
|
|
187
|
+
width: 25.4mm;
|
|
188
|
+
height: 25.4mm;
|
|
189
189
|
padding: 0;
|
|
190
190
|
display: inline-flex;
|
|
191
191
|
align-items: center;
|
|
@@ -208,6 +208,18 @@
|
|
|
208
208
|
padding: 2mm;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/* ---- Dialog: comfortable interior padding ----
|
|
212
|
+
*
|
|
213
|
+
* Same problem the OverlayPanel had — PrimeReact's `.p-dialog-content`
|
|
214
|
+
* lands with near-zero padding so dialog bodies feel squished
|
|
215
|
+
* against the title bar and side borders. Adding 2mm here gives
|
|
216
|
+
* dialogs a consistent inner gutter without each consumer having
|
|
217
|
+
* to wrap their content in their own padded container.
|
|
218
|
+
*/
|
|
219
|
+
.p-dialog-content {
|
|
220
|
+
padding: 2mm;
|
|
221
|
+
}
|
|
222
|
+
|
|
211
223
|
.ac-toolbar-group {
|
|
212
224
|
display: flex;
|
|
213
225
|
flex-direction: row;
|