@adcops/autocore-react 3.3.89 → 3.3.90
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/assets/JogXNeg.d.ts +4 -0
- package/dist/assets/JogXNeg.d.ts.map +1 -0
- package/dist/assets/JogXNeg.js +1 -0
- package/dist/assets/JogXPos.d.ts +4 -0
- package/dist/assets/JogXPos.d.ts.map +1 -0
- package/dist/assets/JogXPos.js +1 -0
- package/dist/assets/JogYNeg.d.ts +4 -0
- package/dist/assets/JogYNeg.d.ts.map +1 -0
- package/dist/assets/JogYNeg.js +1 -0
- package/dist/assets/JogYPos.d.ts +4 -0
- package/dist/assets/JogYPos.d.ts.map +1 -0
- package/dist/assets/JogYPos.js +1 -0
- package/dist/assets/JogZNeg.d.ts +4 -0
- package/dist/assets/JogZNeg.d.ts.map +1 -0
- package/dist/assets/JogZNeg.js +1 -0
- package/dist/assets/JogZPos.d.ts +4 -0
- package/dist/assets/JogZPos.d.ts.map +1 -0
- package/dist/assets/JogZPos.js +1 -0
- package/dist/assets/Off.d.ts +4 -0
- package/dist/assets/Off.d.ts.map +1 -0
- package/dist/assets/Off.js +1 -0
- package/dist/assets/On.d.ts +4 -0
- package/dist/assets/On.d.ts.map +1 -0
- package/dist/assets/On.js +1 -0
- package/dist/assets/index.d.ts +6 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/assets/svg/off.svg +2 -0
- package/dist/assets/svg/on.svg +11 -0
- package/dist/components/JogPanel.d.ts +2 -2
- package/dist/components/JogPanel.d.ts.map +1 -1
- package/dist/components/JogPanel.js +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
- package/dist/components/ams/AssetEditDialog.js +1 -1
- package/dist/components/ams/AssetRegistryTable.css +12 -0
- package/dist/components/ams/AssetRegistryTable.d.ts +1 -0
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/tis/ConfigurationDialog.d.ts +21 -0
- package/dist/components/tis/ConfigurationDialog.d.ts.map +1 -0
- package/dist/components/tis/ConfigurationDialog.js +1 -0
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +27 -0
- 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 +37 -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/components/tis-editor/TisConfigEditor.css +20 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts +19 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ConfigurationsEditor.js +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +13 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +3 -2
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +2 -1
- package/src/assets/JogXNeg.tsx +30 -0
- package/src/assets/JogXPos.tsx +30 -0
- package/src/assets/JogYNeg.tsx +30 -0
- package/src/assets/JogYPos.tsx +30 -0
- package/src/assets/JogZNeg.tsx +30 -0
- package/src/assets/JogZPos.tsx +30 -0
- package/src/assets/Off.tsx +14 -0
- package/src/assets/On.tsx +26 -0
- package/src/assets/index.ts +6 -0
- package/src/assets/svg/off.svg +2 -0
- package/src/assets/svg/on.svg +11 -0
- package/src/components/JogPanel.tsx +18 -28
- package/src/components/ams/AssetDetailView.tsx +1 -1
- package/src/components/ams/AssetEditDialog.tsx +25 -10
- package/src/components/ams/AssetRegistryTable.css +12 -0
- package/src/components/ams/AssetRegistryTable.tsx +15 -4
- package/src/components/tis/ConfigurationDialog.tsx +128 -0
- package/src/components/tis/ResultHistoryTable.tsx +2 -2
- package/src/components/tis/TestDataView.tsx +83 -1
- package/src/components/tis/TestSetupForm.tsx +167 -10
- package/src/components/tis/TisProvider.tsx +53 -0
- package/src/components/tis-editor/TisConfigEditor.css +20 -0
- package/src/components/tis-editor/editor/ConfigurationsEditor.tsx +242 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +4 -0
- package/src/components/tis-editor/types.ts +14 -0
- package/src/components/tis-editor/validation.ts +29 -0
- package/src/themes/adc-dark/_extensions.scss +1 -0
- package/src/themes/theme-base/components/panel/_fieldset.scss +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext, useMemo
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { Button } from 'primereact/button';
|
|
3
3
|
import { InputText } from 'primereact/inputtext';
|
|
4
4
|
import { Dropdown } from 'primereact/dropdown';
|
|
@@ -11,6 +11,7 @@ import { TextInput } from '../TextInput';
|
|
|
11
11
|
import { useTis } from './TisProvider';
|
|
12
12
|
import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
|
|
13
13
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
14
|
+
import { ConfigurationDialog, configLabelOf } from './ConfigurationDialog';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* One asset_ref declared on a test method. We only consume the subset
|
|
@@ -59,6 +60,19 @@ export interface TestFieldDef {
|
|
|
59
60
|
* Cycle and results values are scaled by the corresponding paths
|
|
60
61
|
* in TestDataView; the server scales CSV exports too. */
|
|
61
62
|
scale?: number;
|
|
63
|
+
/** Optional fixed set of choices. When present, the field renders
|
|
64
|
+
* as a dropdown and the operator must pick one of the declared
|
|
65
|
+
* values rather than typing freely. Each entry is either a bare
|
|
66
|
+
* scalar (label === value) or an explicit `{ label, value }` pair
|
|
67
|
+
* when the displayed text should differ from the stored value.
|
|
68
|
+
* Works with any `type`; like `default`, values are authored in
|
|
69
|
+
* display units when `scale` is set. */
|
|
70
|
+
options?: Array<
|
|
71
|
+
| string
|
|
72
|
+
| number
|
|
73
|
+
| boolean
|
|
74
|
+
| { label?: string; value: string | number | boolean }
|
|
75
|
+
>;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export interface TestMethod {
|
|
@@ -90,6 +104,33 @@ export interface TestMethod {
|
|
|
90
104
|
* as `analysis` — the form ignores them; the type just has to
|
|
91
105
|
* accept them so generated schemas typecheck. */
|
|
92
106
|
views?: Record<string, any>;
|
|
107
|
+
/**
|
|
108
|
+
* Optional named configurations for this method. When one or more
|
|
109
|
+
* are declared, the form renders a Configuration selector beneath
|
|
110
|
+
* the Test Method picker. Selecting (or accepting) a configuration
|
|
111
|
+
* writes its `defaults` into the matching config_fields. Each
|
|
112
|
+
* configuration only needs to declare the fields whose value is
|
|
113
|
+
* specific to it — unlisted fields keep the method's base default.
|
|
114
|
+
*/
|
|
115
|
+
configurations?: TestConfiguration[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One named, ready-to-use set of config_field overrides for a method —
|
|
120
|
+
* e.g. translational_traction's "Plaque" vs "Shoe". `defaults` is a
|
|
121
|
+
* sparse map of `config_field.name → value`; values are authored in
|
|
122
|
+
* display units (the form converts via the field's `scale` before
|
|
123
|
+
* writing to stagedConfig / GM, same convention as `TestFieldDef.default`).
|
|
124
|
+
*/
|
|
125
|
+
export interface TestConfiguration {
|
|
126
|
+
/** Canonical key — unique within the method. */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Pretty label shown in the Configuration picker. Falls back to `name`. */
|
|
129
|
+
label?: string;
|
|
130
|
+
/** Long-form guidance shown beside the dropdown in the picker dialog. */
|
|
131
|
+
description?: string;
|
|
132
|
+
/** Sparse `config_field.name → value` overrides applied on selection. */
|
|
133
|
+
defaults?: Record<string, any>;
|
|
93
134
|
}
|
|
94
135
|
|
|
95
136
|
/**
|
|
@@ -145,6 +186,20 @@ const displayToRaw = (display: any, scale: number | undefined): any => {
|
|
|
145
186
|
const hasDescription = (f: TestFieldDef): boolean =>
|
|
146
187
|
typeof f.description === 'string' && f.description.length > 0;
|
|
147
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Normalise a field's `options` (bare scalars and/or `{label, value}`
|
|
191
|
+
* pairs) into the `{ label, value }[]` shape PrimeReact's Dropdown
|
|
192
|
+
* wants. Bare scalars use their stringified form as the label.
|
|
193
|
+
*/
|
|
194
|
+
const normalizeOptions = (
|
|
195
|
+
opts: TestFieldDef['options'],
|
|
196
|
+
): { label: string; value: string | number | boolean }[] =>
|
|
197
|
+
(opts ?? []).map((o) =>
|
|
198
|
+
o !== null && typeof o === 'object'
|
|
199
|
+
? { label: String(o.label ?? o.value), value: o.value }
|
|
200
|
+
: { label: String(o), value: o },
|
|
201
|
+
);
|
|
202
|
+
|
|
148
203
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
149
204
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
150
205
|
|
|
@@ -240,6 +295,28 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
240
295
|
|
|
241
296
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
242
297
|
|
|
298
|
+
// Write one configuration's sparse `defaults` overrides into a config
|
|
299
|
+
// object in place (returns the same object for chaining). Mirrors the
|
|
300
|
+
// base-default seeding below: values are authored in display units, so
|
|
301
|
+
// we convert to raw before storing; source-bound fields are also
|
|
302
|
+
// pushed to GM so the control program sees them. Used both by the
|
|
303
|
+
// auto-apply-first effect and by the picker dialog's accept handler.
|
|
304
|
+
const applyConfigOverridesInto = (cfg: TestConfiguration, target: any): any => {
|
|
305
|
+
for (const [fieldName, val] of Object.entries(cfg.defaults ?? {})) {
|
|
306
|
+
if (fieldName === 'sample_id') continue;
|
|
307
|
+
const field = schema?.config_fields.find((f: TestFieldDef) => f.name === fieldName);
|
|
308
|
+
const rawVal = displayToRaw(val, field?.scale);
|
|
309
|
+
target[fieldName] = rawVal;
|
|
310
|
+
if (field?.source) {
|
|
311
|
+
void Promise.resolve()
|
|
312
|
+
.then(() => write(field.source!, rawVal))
|
|
313
|
+
.catch(e => console.error(
|
|
314
|
+
`[TestSetupForm] Failed to apply configuration override for ${fieldName}:`, e));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return target;
|
|
318
|
+
};
|
|
319
|
+
|
|
243
320
|
useEffect(() => {
|
|
244
321
|
if (tis.selection.methodId !== methodId && methodId) {
|
|
245
322
|
tis.setSelection({ methodId });
|
|
@@ -274,6 +351,19 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
274
351
|
|
|
275
352
|
const [isValid, setIsValid] = useState(false);
|
|
276
353
|
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
354
|
+
const [configPickerOpen, setConfigPickerOpen] = useState(false);
|
|
355
|
+
|
|
356
|
+
const configurations = (schema?.configurations ?? []) as TestConfiguration[];
|
|
357
|
+
const selectedConfig = configurations.find(c => c.name === tis.configurationName);
|
|
358
|
+
|
|
359
|
+
// Operator accepted a configuration in the picker dialog — write its
|
|
360
|
+
// overrides into the fields and remember the selection.
|
|
361
|
+
const handleConfigurationSelected = (configName: string) => {
|
|
362
|
+
const cfg = configurations.find(c => c.name === configName);
|
|
363
|
+
if (!cfg) return;
|
|
364
|
+
setConfig((prev: any) => applyConfigOverridesInto(cfg, { ...prev }));
|
|
365
|
+
tis.setConfigurationName(configName);
|
|
366
|
+
};
|
|
277
367
|
|
|
278
368
|
// Single shared "info" dialog used by:
|
|
279
369
|
// - the Test Setup status button (shows what's wrong, including
|
|
@@ -288,14 +378,23 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
288
378
|
// Apply schema-declared defaults when the operator picks a method.
|
|
289
379
|
// Source-bound fields write the default to GM (the control program
|
|
290
380
|
// is the consumer of record); non-source fields land directly in
|
|
291
|
-
// stagedConfig.
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
381
|
+
// stagedConfig. The "already seeded" marker lives on the provider
|
|
382
|
+
// (tis.defaultsAppliedForMethod), not in a local ref, so it
|
|
383
|
+
// survives this form unmounting when the operator switches tabs —
|
|
384
|
+
// otherwise defaults would re-apply on every Test-tab return and
|
|
385
|
+
// clobber the operator's edits. Only an actual method change (or
|
|
386
|
+
// clearStagedConfig() after start_test) re-applies the defaults.
|
|
295
387
|
useEffect(() => {
|
|
296
388
|
if (!schema || !methodId) return;
|
|
297
|
-
if (
|
|
298
|
-
|
|
389
|
+
if (tis.defaultsAppliedForMethod === methodId) return;
|
|
390
|
+
tis.markDefaultsAppliedForMethod(methodId);
|
|
391
|
+
|
|
392
|
+
// Auto-apply the method's first configuration (if any) on top of
|
|
393
|
+
// the base defaults. '' when the method declares none, which also
|
|
394
|
+
// clears any stale selection carried over from a previous method.
|
|
395
|
+
const firstConfig = (schema.configurations && schema.configurations.length > 0)
|
|
396
|
+
? schema.configurations[0] as TestConfiguration
|
|
397
|
+
: undefined;
|
|
299
398
|
|
|
300
399
|
setConfig((prev: any) => {
|
|
301
400
|
let next = prev;
|
|
@@ -321,9 +420,17 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
321
420
|
`[TestSetupForm] Failed to seed default for ${field.name}:`, e));
|
|
322
421
|
}
|
|
323
422
|
}
|
|
423
|
+
// Configuration overrides win over base defaults for the
|
|
424
|
+
// fields they name (e.g. Plaque's z_start_position).
|
|
425
|
+
if (firstConfig) {
|
|
426
|
+
if (next === prev) next = { ...prev };
|
|
427
|
+
next = applyConfigOverridesInto(firstConfig, next);
|
|
428
|
+
}
|
|
324
429
|
return next;
|
|
325
430
|
});
|
|
326
|
-
|
|
431
|
+
|
|
432
|
+
tis.setConfigurationName(firstConfig ? firstConfig.name : '');
|
|
433
|
+
}, [schema, methodId, write, tis.defaultsAppliedForMethod, tis.markDefaultsAppliedForMethod, tis.setConfigurationName]);
|
|
327
434
|
|
|
328
435
|
// Seed and live-update config_fields that declare a `source`.
|
|
329
436
|
useEffect(() => {
|
|
@@ -451,7 +558,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
451
558
|
if (field.name === 'sample_id') return null;
|
|
452
559
|
const valid = isFieldValid(field);
|
|
453
560
|
const assetType = assetTypeForField(field);
|
|
454
|
-
|
|
561
|
+
// A schema-declared `options` list turns the field into a
|
|
562
|
+
// dropdown (skipped when an asset_ref already claims the field
|
|
563
|
+
// — that picker is sourced from AMS, not the static list).
|
|
564
|
+
const dropdownOptions = !assetType && Array.isArray(field.options) && field.options.length > 0
|
|
565
|
+
? normalizeOptions(field.options)
|
|
566
|
+
: null;
|
|
567
|
+
const isNum = !assetType && !dropdownOptions
|
|
568
|
+
&& field.type !== 'string' && field.type !== 'bool';
|
|
455
569
|
return (
|
|
456
570
|
<React.Fragment key={field.name}>
|
|
457
571
|
<span className="ac-form-label">{labelOf(field)}</span>
|
|
@@ -462,6 +576,15 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
462
576
|
onChange={(val) => handleFieldChange(field, val)}
|
|
463
577
|
invalid={!valid}
|
|
464
578
|
/>
|
|
579
|
+
) : dropdownOptions ? (
|
|
580
|
+
<Dropdown
|
|
581
|
+
value={config[field.name] ?? null}
|
|
582
|
+
options={dropdownOptions}
|
|
583
|
+
onChange={(e) => handleFieldChange(field, e.value)}
|
|
584
|
+
placeholder={`Select ${field.label ?? field.name}…`}
|
|
585
|
+
className={!valid ? 'p-invalid' : ''}
|
|
586
|
+
showClear={!field.required}
|
|
587
|
+
/>
|
|
465
588
|
) : isNum ? (
|
|
466
589
|
<ValueInput
|
|
467
590
|
label={undefined}
|
|
@@ -664,7 +787,7 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
664
787
|
tabIndex={-1}
|
|
665
788
|
/>
|
|
666
789
|
<Button
|
|
667
|
-
icon="pi pi-
|
|
790
|
+
icon="pi pi-folder"
|
|
668
791
|
type="button"
|
|
669
792
|
onClick={() => setMethodPickerOpen(true)}
|
|
670
793
|
tooltip={methodIds.length > 1
|
|
@@ -680,6 +803,32 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
680
803
|
</>
|
|
681
804
|
)}
|
|
682
805
|
|
|
806
|
+
{configurations.length > 0 && (
|
|
807
|
+
<>
|
|
808
|
+
<span className="ac-form-label">Configuration</span>
|
|
809
|
+
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
810
|
+
<InputText
|
|
811
|
+
value={selectedConfig ? configLabelOf(selectedConfig) : ''}
|
|
812
|
+
placeholder="No configuration selected"
|
|
813
|
+
readOnly
|
|
814
|
+
style={{ flex: 1 }}
|
|
815
|
+
tabIndex={-1}
|
|
816
|
+
/>
|
|
817
|
+
<Button
|
|
818
|
+
icon="pi pi-folder"
|
|
819
|
+
type="button"
|
|
820
|
+
onClick={() => setConfigPickerOpen(true)}
|
|
821
|
+
tooltip="Change configuration"
|
|
822
|
+
tooltipOptions={{ position: 'top' }}
|
|
823
|
+
/>
|
|
824
|
+
</div>
|
|
825
|
+
<span aria-hidden="true" />
|
|
826
|
+
<span style={{ color: selectedConfig ? 'var(--green-500)' : 'var(--text-secondary-color)', display: 'flex', alignItems: 'center' }}>
|
|
827
|
+
<i className={selectedConfig ? 'pi pi-check' : 'pi pi-minus'} />
|
|
828
|
+
</span>
|
|
829
|
+
</>
|
|
830
|
+
)}
|
|
831
|
+
|
|
683
832
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
684
833
|
{schema.config_fields.map(renderConfigField)}
|
|
685
834
|
|
|
@@ -690,6 +839,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
690
839
|
onSelected={(picked) => setMethodIdLocal(picked)}
|
|
691
840
|
/>
|
|
692
841
|
|
|
842
|
+
<ConfigurationDialog
|
|
843
|
+
visible={configPickerOpen}
|
|
844
|
+
onHide={() => setConfigPickerOpen(false)}
|
|
845
|
+
configurations={configurations}
|
|
846
|
+
currentConfigName={tis.configurationName}
|
|
847
|
+
onSelected={handleConfigurationSelected}
|
|
848
|
+
/>
|
|
849
|
+
|
|
693
850
|
{/* Shared info dialog. Driven by the Test Setup status
|
|
694
851
|
button (validation + server errors) and by per-field
|
|
695
852
|
info buttons (replaces the old hover Tooltip). One
|
|
@@ -181,6 +181,33 @@ export interface TisContextValue {
|
|
|
181
181
|
* method selection or after a run completes. */
|
|
182
182
|
clearStagedConfig: () => void;
|
|
183
183
|
|
|
184
|
+
/** methodId whose schema defaults have already been seeded into
|
|
185
|
+
* `stagedConfig` (and written to GM for source-bound fields) for
|
|
186
|
+
* the current session. `<TestSetupForm>` gates its one-shot
|
|
187
|
+
* "apply defaults" effect on this value so it only fires on an
|
|
188
|
+
* actual method change — not on form remount when the operator
|
|
189
|
+
* switches tabs. Empty string means "no method seeded yet."
|
|
190
|
+
* Reset to `''` by `clearStagedConfig()` so the next staged test
|
|
191
|
+
* re-seeds correctly. */
|
|
192
|
+
defaultsAppliedForMethod: string;
|
|
193
|
+
/** Mark the given methodId as having been seeded. The form calls
|
|
194
|
+
* this immediately after writing the defaults, so subsequent
|
|
195
|
+
* remounts skip re-application. */
|
|
196
|
+
markDefaultsAppliedForMethod: (methodId: string) => void;
|
|
197
|
+
|
|
198
|
+
/** `name` of the method configuration the operator currently has
|
|
199
|
+
* selected (see `TestMethod.configurations`). Empty string when the
|
|
200
|
+
* active method declares no configurations, or none is chosen yet.
|
|
201
|
+
* Held on the provider — like `stagedConfig` — so the selection
|
|
202
|
+
* survives `<TestSetupForm>` remounting on a tab switch. HMI-only:
|
|
203
|
+
* it scopes which override set is shown; the override *values* it
|
|
204
|
+
* applies are what actually get recorded. */
|
|
205
|
+
configurationName: string;
|
|
206
|
+
/** Set the selected configuration name. The form calls this when the
|
|
207
|
+
* seeding effect auto-applies the first configuration and when the
|
|
208
|
+
* operator accepts a different one in the picker dialog. */
|
|
209
|
+
setConfigurationName: (name: string) => void;
|
|
210
|
+
|
|
184
211
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
185
212
|
* omitted to aggregate runs across every method in the project —
|
|
186
213
|
* the History tab uses this. */
|
|
@@ -224,6 +251,10 @@ const TisContext = createContext<TisContextValue>({
|
|
|
224
251
|
stagedConfig: {},
|
|
225
252
|
setStagedConfig: () => {},
|
|
226
253
|
clearStagedConfig: () => {},
|
|
254
|
+
defaultsAppliedForMethod: '',
|
|
255
|
+
markDefaultsAppliedForMethod: () => {},
|
|
256
|
+
configurationName: '',
|
|
257
|
+
setConfigurationName: () => {},
|
|
227
258
|
fetchRuns: async () => [],
|
|
228
259
|
fetchRun: async () => null,
|
|
229
260
|
runCache: {},
|
|
@@ -646,8 +677,26 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
646
677
|
const setStagedConfig = useCallback((patch: Record<string, any>) => {
|
|
647
678
|
setStagedConfigState(prev => ({ ...prev, ...patch }));
|
|
648
679
|
}, []);
|
|
680
|
+
// Tracks which methodId's schema defaults have been seeded into
|
|
681
|
+
// stagedConfig + GM during this session. Owned by the provider (not
|
|
682
|
+
// by <TestSetupForm>) so the marker survives the form unmounting
|
|
683
|
+
// when the operator switches tabs — otherwise the form's seed
|
|
684
|
+
// effect re-fires on every remount and clobbers operator edits.
|
|
685
|
+
const [defaultsAppliedForMethod, setDefaultsAppliedForMethod] = useState<string>('');
|
|
686
|
+
const markDefaultsAppliedForMethod = useCallback((methodId: string) => {
|
|
687
|
+
setDefaultsAppliedForMethod(methodId);
|
|
688
|
+
}, []);
|
|
689
|
+
// Selected method configuration (TestMethod.configurations). Lives
|
|
690
|
+
// alongside stagedConfig so it survives a form remount; reset on a
|
|
691
|
+
// fresh stage so the next test re-applies the method's first config.
|
|
692
|
+
const [configurationName, setConfigurationName] = useState<string>('');
|
|
649
693
|
const clearStagedConfig = useCallback(() => {
|
|
650
694
|
setStagedConfigState({});
|
|
695
|
+
// Re-seed defaults on the next stage. Without this, the form
|
|
696
|
+
// would render empty after start_test (config cleared) because
|
|
697
|
+
// the seed gate would still consider the method "applied."
|
|
698
|
+
setDefaultsAppliedForMethod('');
|
|
699
|
+
setConfigurationName('');
|
|
651
700
|
}, []);
|
|
652
701
|
|
|
653
702
|
const value: TisContextValue = useMemo(() => ({
|
|
@@ -656,6 +705,8 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
656
705
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
657
706
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
658
707
|
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
708
|
+
defaultsAppliedForMethod, markDefaultsAppliedForMethod,
|
|
709
|
+
configurationName, setConfigurationName,
|
|
659
710
|
fetchRuns, fetchRun, runCache,
|
|
660
711
|
}), [
|
|
661
712
|
schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
|
|
@@ -663,6 +714,8 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
663
714
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
664
715
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
665
716
|
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
717
|
+
defaultsAppliedForMethod, markDefaultsAppliedForMethod,
|
|
718
|
+
configurationName, setConfigurationName,
|
|
666
719
|
fetchRuns, fetchRun, runCache,
|
|
667
720
|
]);
|
|
668
721
|
|
|
@@ -119,3 +119,23 @@
|
|
|
119
119
|
gap: 0.25rem;
|
|
120
120
|
margin-bottom: 0.25rem;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/* Portrait displays (e.g. wall-mounted HMIs rotated to portrait) don't
|
|
124
|
+
have the horizontal room for a fixed 320px selection pane beside the
|
|
125
|
+
content pane — the content pane gets squeezed and cut off. Stack the
|
|
126
|
+
two panes instead: the method-selection list becomes a capped,
|
|
127
|
+
scrollable band on top, and the editor takes the remaining height. */
|
|
128
|
+
@media (orientation: portrait) {
|
|
129
|
+
.tis-editor__body {
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.tis-editor__sidebar {
|
|
134
|
+
/* Cap the list at ~35% of the body height (it scrolls internally
|
|
135
|
+
via the DataTable's scrollHeight="flex"); the editor below gets
|
|
136
|
+
the rest. Drop the fixed 320px width — full width when stacked. */
|
|
137
|
+
flex: 0 1 35%;
|
|
138
|
+
border-right: none;
|
|
139
|
+
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigurationsEditor — manages a method's named `configurations`
|
|
3
|
+
* (e.g. translational_traction's "Plaque" vs "Shoe"). Each configuration
|
|
4
|
+
* carries a sparse set of config_field overrides; only the fields whose
|
|
5
|
+
* value is specific to that configuration need to be listed.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors <AssetRefsEditor>: a DataTable of the declared configurations
|
|
8
|
+
* plus an add/edit Dialog. The override sub-editor is a small key/value
|
|
9
|
+
* table whose key is a dropdown of the method's own config_fields, so the
|
|
10
|
+
* author can only target fields that actually exist, and values are
|
|
11
|
+
* coerced to the field's declared type on save.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useMemo, useState } from 'react';
|
|
15
|
+
import { Dialog } from 'primereact/dialog';
|
|
16
|
+
import { Button } from 'primereact/button';
|
|
17
|
+
import { InputText } from 'primereact/inputtext';
|
|
18
|
+
import { InputTextarea } from 'primereact/inputtextarea';
|
|
19
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
20
|
+
import { DataTable } from 'primereact/datatable';
|
|
21
|
+
import { Column } from 'primereact/column';
|
|
22
|
+
import { FormSection } from '../../forms/FormSection';
|
|
23
|
+
import { FormRow } from '../../forms/FormRow';
|
|
24
|
+
import type { TestConfiguration, TestField, TestMethod } from '../types';
|
|
25
|
+
|
|
26
|
+
export interface ConfigurationsEditorProps {
|
|
27
|
+
method: TestMethod;
|
|
28
|
+
onChange: (next: TestMethod) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NUMERIC_TYPES = new Set(['i32', 'i64', 'u32', 'u64', 'f32', 'f64']);
|
|
32
|
+
|
|
33
|
+
/** Coerce a raw text entry to the declared field type. Non-numeric text
|
|
34
|
+
* for a numeric field is left as the string so validation can flag it. */
|
|
35
|
+
const coerceValue = (field: TestField | undefined, raw: string): unknown => {
|
|
36
|
+
const t = field?.type ?? 'string';
|
|
37
|
+
if (t === 'bool') return raw === 'true' || raw === '1';
|
|
38
|
+
if (NUMERIC_TYPES.has(t)) {
|
|
39
|
+
const n = Number(raw);
|
|
40
|
+
return raw.trim() !== '' && Number.isFinite(n) ? n : raw;
|
|
41
|
+
}
|
|
42
|
+
return raw;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
interface OverrideRow { field: string; value: string }
|
|
46
|
+
|
|
47
|
+
const blank = (): TestConfiguration => ({ name: '', defaults: {} });
|
|
48
|
+
|
|
49
|
+
const toRows = (defaults: Record<string, unknown> | undefined): OverrideRow[] =>
|
|
50
|
+
Object.entries(defaults ?? {}).map(([field, value]) => ({ field, value: String(value) }));
|
|
51
|
+
|
|
52
|
+
export const ConfigurationsEditor: React.FC<ConfigurationsEditorProps> = ({ method, onChange }) => {
|
|
53
|
+
const configs: TestConfiguration[] = (method.configurations as TestConfiguration[]) ?? [];
|
|
54
|
+
const configFields: TestField[] = (method.config_fields as TestField[]) ?? [];
|
|
55
|
+
|
|
56
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
57
|
+
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
|
58
|
+
const [draft, setDraft] = useState<TestConfiguration>(blank());
|
|
59
|
+
const [rows, setRows] = useState<OverrideRow[]>([]);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
// Fields the author can override (skip the implicit sample_id).
|
|
63
|
+
const fieldOptions = useMemo(
|
|
64
|
+
() => configFields
|
|
65
|
+
.filter(f => f.name && f.name !== 'sample_id')
|
|
66
|
+
.map(f => ({ label: f.units ? `${f.name} [${f.units}]` : f.name, value: f.name })),
|
|
67
|
+
[configFields],
|
|
68
|
+
);
|
|
69
|
+
const fieldByName = useMemo(() => {
|
|
70
|
+
const m = new Map<string, TestField>();
|
|
71
|
+
configFields.forEach(f => m.set(f.name, f));
|
|
72
|
+
return m;
|
|
73
|
+
}, [configFields]);
|
|
74
|
+
|
|
75
|
+
const openNew = () => {
|
|
76
|
+
setEditingIdx(null);
|
|
77
|
+
setDraft(blank());
|
|
78
|
+
setRows([]);
|
|
79
|
+
setError(null);
|
|
80
|
+
setDialogOpen(true);
|
|
81
|
+
};
|
|
82
|
+
const openEdit = (i: number) => {
|
|
83
|
+
setEditingIdx(i);
|
|
84
|
+
setDraft({ ...configs[i] });
|
|
85
|
+
setRows(toRows(configs[i].defaults));
|
|
86
|
+
setError(null);
|
|
87
|
+
setDialogOpen(true);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const validate = (c: TestConfiguration): string | null => {
|
|
91
|
+
if (!c.name.trim()) return 'Name is required.';
|
|
92
|
+
if (configs.some((o, i) => o.name === c.name && i !== editingIdx)) {
|
|
93
|
+
return `A configuration named "${c.name}" already exists.`;
|
|
94
|
+
}
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
for (const r of rows) {
|
|
97
|
+
if (!r.field) return 'Every override row must pick a field.';
|
|
98
|
+
if (seen.has(r.field)) return `Field "${r.field}" is overridden more than once.`;
|
|
99
|
+
seen.add(r.field);
|
|
100
|
+
if (!fieldByName.has(r.field)) return `"${r.field}" is not a config_field on this method.`;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleSave = () => {
|
|
106
|
+
const err = validate(draft);
|
|
107
|
+
if (err) { setError(err); return; }
|
|
108
|
+
const defaults: Record<string, unknown> = {};
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
defaults[r.field] = coerceValue(fieldByName.get(r.field), r.value);
|
|
111
|
+
}
|
|
112
|
+
const saved: TestConfiguration = {
|
|
113
|
+
name: draft.name.trim(),
|
|
114
|
+
...(draft.label?.trim() ? { label: draft.label.trim() } : {}),
|
|
115
|
+
...(draft.description?.trim() ? { description: draft.description.trim() } : {}),
|
|
116
|
+
defaults,
|
|
117
|
+
};
|
|
118
|
+
const next = [...configs];
|
|
119
|
+
if (editingIdx === null) next.push(saved);
|
|
120
|
+
else next[editingIdx] = saved;
|
|
121
|
+
onChange({ ...method, configurations: next });
|
|
122
|
+
setDialogOpen(false);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleRemove = (i: number) => {
|
|
126
|
+
if (!window.confirm(`Remove configuration "${configs[i].name}"?`)) return;
|
|
127
|
+
onChange({ ...method, configurations: configs.filter((_, idx) => idx !== i) });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const addRow = () => setRows([...rows, { field: '', value: '' }]);
|
|
131
|
+
const updateRow = (i: number, patch: Partial<OverrideRow>) =>
|
|
132
|
+
setRows(rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r)));
|
|
133
|
+
const removeRow = (i: number) => setRows(rows.filter((_, idx) => idx !== i));
|
|
134
|
+
|
|
135
|
+
const overridesSummary = (c: TestConfiguration): string => {
|
|
136
|
+
const entries = Object.entries(c.defaults ?? {});
|
|
137
|
+
if (entries.length === 0) return '—';
|
|
138
|
+
return entries.map(([k, v]) => `${k}=${String(v)}`).join(', ');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const rowActions = (_r: TestConfiguration, opts: { rowIndex: number }) => (
|
|
142
|
+
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
143
|
+
<Button icon="pi pi-pencil" className="p-button-text p-button-sm" onClick={() => openEdit(opts.rowIndex)} />
|
|
144
|
+
<Button icon="pi pi-trash" className="p-button-text p-button-danger p-button-sm" onClick={() => handleRemove(opts.rowIndex)} />
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<FormSection
|
|
151
|
+
title="Configurations"
|
|
152
|
+
description="Named, ready-to-use override sets (e.g. Plaque vs Shoe). Selecting one in Test Setup writes its values into the matching config fields; the method's first configuration is applied automatically."
|
|
153
|
+
actions={<Button label="Add configuration" icon="pi pi-plus" size="small" onClick={openNew} />}
|
|
154
|
+
>
|
|
155
|
+
<DataTable value={configs} dataKey="name" emptyMessage="No configurations declared.">
|
|
156
|
+
<Column field="name" header="Name" style={{ width: '10rem' }} />
|
|
157
|
+
<Column header="Label" body={(c: TestConfiguration) => c.label ?? '—'} style={{ width: '10rem' }} />
|
|
158
|
+
<Column header="Overrides" body={overridesSummary} />
|
|
159
|
+
<Column header="" body={rowActions} style={{ width: '6rem' }} />
|
|
160
|
+
</DataTable>
|
|
161
|
+
</FormSection>
|
|
162
|
+
|
|
163
|
+
<Dialog
|
|
164
|
+
header={editingIdx === null ? 'New configuration' : `Edit configuration: ${configs[editingIdx ?? 0]?.name}`}
|
|
165
|
+
visible={dialogOpen}
|
|
166
|
+
onHide={() => setDialogOpen(false)}
|
|
167
|
+
style={{ width: '44rem' }}
|
|
168
|
+
>
|
|
169
|
+
{error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
|
|
170
|
+
<FormRow label="Name" required hint="Canonical key — unique within the method.">
|
|
171
|
+
<InputText value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} placeholder="e.g. plaque" />
|
|
172
|
+
</FormRow>
|
|
173
|
+
<FormRow label="Label" hint="Shown in the Configuration picker. Falls back to Name.">
|
|
174
|
+
<InputText value={draft.label ?? ''} onChange={(e) => setDraft({ ...draft, label: e.target.value || undefined })} placeholder="e.g. Plaque" />
|
|
175
|
+
</FormRow>
|
|
176
|
+
<FormRow label="Description" hint="Shown beside the dropdown in the picker dialog.">
|
|
177
|
+
<InputTextarea rows={2} value={draft.description ?? ''} onChange={(e) => setDraft({ ...draft, description: e.target.value || undefined })} />
|
|
178
|
+
</FormRow>
|
|
179
|
+
|
|
180
|
+
<FormSection
|
|
181
|
+
title="Field overrides"
|
|
182
|
+
description="Only list fields whose value is specific to this configuration. Values are authored in display units, same as a field's default."
|
|
183
|
+
actions={
|
|
184
|
+
<Button
|
|
185
|
+
label="Add override"
|
|
186
|
+
icon="pi pi-plus"
|
|
187
|
+
size="small"
|
|
188
|
+
onClick={addRow}
|
|
189
|
+
disabled={fieldOptions.length === 0}
|
|
190
|
+
/>
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
{fieldOptions.length === 0 ? (
|
|
194
|
+
<small style={{ color: 'var(--text-secondary-color)' }}>
|
|
195
|
+
This method has no config_fields to override yet — add some on the Fields tab first.
|
|
196
|
+
</small>
|
|
197
|
+
) : rows.length === 0 ? (
|
|
198
|
+
<small style={{ color: 'var(--text-secondary-color)' }}>No overrides — this configuration uses every field's base default.</small>
|
|
199
|
+
) : (
|
|
200
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
201
|
+
{rows.map((r, i) => {
|
|
202
|
+
const f = fieldByName.get(r.field);
|
|
203
|
+
return (
|
|
204
|
+
<div key={i} style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
|
205
|
+
<Dropdown
|
|
206
|
+
value={r.field || null}
|
|
207
|
+
options={fieldOptions}
|
|
208
|
+
onChange={(e) => updateRow(i, { field: e.value })}
|
|
209
|
+
placeholder="Field"
|
|
210
|
+
style={{ flex: '0 0 16rem' }}
|
|
211
|
+
/>
|
|
212
|
+
{f?.type === 'bool' ? (
|
|
213
|
+
<Dropdown
|
|
214
|
+
value={r.value === 'true' || r.value === '1' ? 'true' : 'false'}
|
|
215
|
+
options={[{ label: 'true', value: 'true' }, { label: 'false', value: 'false' }]}
|
|
216
|
+
onChange={(e) => updateRow(i, { value: e.value })}
|
|
217
|
+
style={{ flex: 1 }}
|
|
218
|
+
/>
|
|
219
|
+
) : (
|
|
220
|
+
<InputText
|
|
221
|
+
value={r.value}
|
|
222
|
+
onChange={(e) => updateRow(i, { value: e.target.value })}
|
|
223
|
+
placeholder={f?.type && NUMERIC_TYPES.has(f.type) ? 'number' : 'value'}
|
|
224
|
+
style={{ flex: 1 }}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
<Button icon="pi pi-trash" className="p-button-text p-button-danger p-button-sm" onClick={() => removeRow(i)} />
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</FormSection>
|
|
234
|
+
|
|
235
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
|
|
236
|
+
<Button label="Cancel" className="p-button-text" onClick={() => setDialogOpen(false)} />
|
|
237
|
+
<Button label="Save" onClick={handleSave} />
|
|
238
|
+
</div>
|
|
239
|
+
</Dialog>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
@@ -13,6 +13,7 @@ import { Button } from 'primereact/button';
|
|
|
13
13
|
import { CodeEditor } from '../../CodeEditor';
|
|
14
14
|
import { IdentitySection } from './IdentitySection';
|
|
15
15
|
import { FieldArrayEditor } from './FieldArrayEditor';
|
|
16
|
+
import { ConfigurationsEditor } from './ConfigurationsEditor';
|
|
16
17
|
import { ViewsEditor } from './ViewsEditor';
|
|
17
18
|
import { RawDataEditor } from './RawDataEditor';
|
|
18
19
|
import { AssetRefsEditor } from './AssetRefsEditor';
|
|
@@ -140,6 +141,9 @@ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
|
|
|
140
141
|
<FieldArrayEditor arrayKey="cycle_fields" method={draft} onChange={handleFormChange} />
|
|
141
142
|
<FieldArrayEditor arrayKey="results_fields" method={draft} onChange={handleFormChange} />
|
|
142
143
|
</TabPanel>
|
|
144
|
+
<TabPanel header="Configurations">
|
|
145
|
+
<ConfigurationsEditor method={draft} onChange={handleFormChange} />
|
|
146
|
+
</TabPanel>
|
|
143
147
|
<TabPanel header="Views">
|
|
144
148
|
<ViewsEditor method={draft} onChange={handleFormChange} />
|
|
145
149
|
</TabPanel>
|