@adcops/autocore-react 3.3.115 → 3.3.116
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/index.d.ts +10 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/CycleConfigDialog.d.ts +39 -0
- package/dist/components/tis/CycleConfigDialog.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigDialog.js +1 -0
- package/dist/components/tis/CycleConfigForm.d.ts +22 -0
- package/dist/components/tis/CycleConfigForm.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigForm.js +1 -0
- package/dist/components/tis/CycleStatusStrip.d.ts +10 -0
- package/dist/components/tis/CycleStatusStrip.d.ts.map +1 -0
- package/dist/components/tis/CycleStatusStrip.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +4 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestFieldRow.d.ts +151 -0
- package/dist/components/tis/TestFieldRow.d.ts.map +1 -0
- package/dist/components/tis/TestFieldRow.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +15 -51
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TestSetupView.d.ts.map +1 -1
- package/dist/components/tis/TestSetupView.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +80 -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.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts +19 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.js +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- 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/editor/TestFieldDialog.d.ts +3 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +28 -1
- 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/package.json +1 -1
- package/src/components/index.ts +14 -0
- package/src/components/tis/CycleConfigDialog.tsx +173 -0
- package/src/components/tis/CycleConfigForm.tsx +192 -0
- package/src/components/tis/CycleStatusStrip.tsx +84 -0
- package/src/components/tis/TestDataView.tsx +19 -3
- package/src/components/tis/TestFieldRow.tsx +297 -0
- package/src/components/tis/TestSetupForm.tsx +52 -212
- package/src/components/tis/TestSetupView.tsx +60 -3
- package/src/components/tis/TisProvider.tsx +191 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +1 -0
- package/src/components/tis-editor/editor/CycleConfigSection.tsx +93 -0
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +10 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +10 -4
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +22 -2
- package/src/components/tis-editor/editor/ViewsEditor.tsx +3 -0
- package/src/components/tis-editor/types.ts +34 -1
- package/src/components/tis-editor/validation.ts +20 -2
package/src/components/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
useTisSelection,
|
|
16
16
|
useTisRuns,
|
|
17
17
|
useTisRun,
|
|
18
|
+
useTisCycleConfig,
|
|
18
19
|
} from './tis/TisProvider';
|
|
19
20
|
export type {
|
|
20
21
|
TisProviderProps,
|
|
@@ -25,6 +26,7 @@ export type {
|
|
|
25
26
|
TisRunCacheEntry,
|
|
26
27
|
SchemaRegistry,
|
|
27
28
|
TisMethodSchema,
|
|
29
|
+
TisCycleConfigState,
|
|
28
30
|
} from './tis/TisProvider';
|
|
29
31
|
|
|
30
32
|
export { ProjectSelector } from './tis/ProjectSelector';
|
|
@@ -36,6 +38,18 @@ export type { TestSetupFormProps } from './tis/TestSetupForm';
|
|
|
36
38
|
export { TestSetupView } from './tis/TestSetupView';
|
|
37
39
|
export type { TestSetupViewProps } from './tis/TestSetupView';
|
|
38
40
|
|
|
41
|
+
export { TestFieldRow } from './tis/TestFieldRow';
|
|
42
|
+
export type { TestFieldRowProps, CycleConfigScope } from './tis/TestFieldRow';
|
|
43
|
+
|
|
44
|
+
export { CycleConfigForm, cycleConfigSummary } from './tis/CycleConfigForm';
|
|
45
|
+
export type { CycleConfigFormProps } from './tis/CycleConfigForm';
|
|
46
|
+
|
|
47
|
+
export { CycleConfigDialog, useCycleConfigGate } from './tis/CycleConfigDialog';
|
|
48
|
+
export type { CycleConfigDialogProps, CycleConfigGate } from './tis/CycleConfigDialog';
|
|
49
|
+
|
|
50
|
+
export { CycleStatusStrip } from './tis/CycleStatusStrip';
|
|
51
|
+
export type { CycleStatusStripProps } from './tis/CycleStatusStrip';
|
|
52
|
+
|
|
39
53
|
export { ProjectInfoDialog } from './tis/ProjectInfoDialog';
|
|
40
54
|
export type { ProjectInfoDialogProps, ProjectInfoMode } from './tis/ProjectInfoDialog';
|
|
41
55
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <CycleConfigDialog> — the `prompt_before_cycle` entry dialog.
|
|
5
|
+
*
|
|
6
|
+
* Some methods can't tolerate a mislabelled row: getting the position
|
|
7
|
+
* wrong means re-running the sample. Those declare
|
|
8
|
+
* `prompt_before_cycle: true`, and this dialog puts the entry in front of
|
|
9
|
+
* the operator at the boundary instead of trusting them to have filled
|
|
10
|
+
* the Cycle tab. It opens on the server-computed `prompt_due` edge — a
|
|
11
|
+
* boundary reached with nothing latched — so the scope rules stay in one
|
|
12
|
+
* place (the server) rather than being re-derived here.
|
|
13
|
+
*
|
|
14
|
+
* Two ways to use it:
|
|
15
|
+
*
|
|
16
|
+
* 1. Mounted plainly, it opens by itself whenever a prompt comes due:
|
|
17
|
+
*
|
|
18
|
+
* <CycleConfigDialog />
|
|
19
|
+
*
|
|
20
|
+
* 2. Driven from your Start handler, so the press itself opens it when
|
|
21
|
+
* the annotation is missing and runs the start when it isn't:
|
|
22
|
+
*
|
|
23
|
+
* const gate = useCycleConfigGate();
|
|
24
|
+
* <Button label="Start" onClick={() => gate.run(startTest)} />
|
|
25
|
+
* <CycleConfigDialog controller={gate} />
|
|
26
|
+
*
|
|
27
|
+
* The second form is the one to reach for on a machine whose Start lives
|
|
28
|
+
* in the footer: the press is never silently swallowed — it either starts
|
|
29
|
+
* the test or explains, in a dialog, exactly what is missing.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
33
|
+
import { Button } from 'primereact/button';
|
|
34
|
+
import { Dialog } from 'primereact/dialog';
|
|
35
|
+
import { useTis } from './TisProvider';
|
|
36
|
+
import { CycleConfigForm } from './CycleConfigForm';
|
|
37
|
+
import { labelOf } from './TestFieldRow';
|
|
38
|
+
|
|
39
|
+
/** Handle returned by [`useCycleConfigGate`]. */
|
|
40
|
+
export interface CycleConfigGate {
|
|
41
|
+
/** True while the dialog should be open. */
|
|
42
|
+
open: boolean;
|
|
43
|
+
setOpen: (open: boolean) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Gate an action on the annotation being complete. When the entry is
|
|
46
|
+
* ready, `action` runs immediately. When it isn't, the dialog opens
|
|
47
|
+
* instead and `action` is held until the operator completes the fields
|
|
48
|
+
* and confirms — so a Start press is deferred, never dropped.
|
|
49
|
+
*/
|
|
50
|
+
run: (action: () => void) => void;
|
|
51
|
+
/** Run the held action and close. Called by the dialog's confirm. */
|
|
52
|
+
confirm: () => void;
|
|
53
|
+
/** Drop the held action and close. */
|
|
54
|
+
cancel: () => void;
|
|
55
|
+
/** True when the gate is blocking (required fields empty). */
|
|
56
|
+
blocked: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wire a Start affordance to the Cycle Configuration gate.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately does NOT return a `disabled` flag for Start. A greyed-out
|
|
63
|
+
* Start in the corner of a touchscreen reads as "the machine is broken"
|
|
64
|
+
* and the operator's next move is a phone call; a press that opens a
|
|
65
|
+
* dialog naming the missing field teaches the workflow in one go.
|
|
66
|
+
*/
|
|
67
|
+
export const useCycleConfigGate = (): CycleConfigGate => {
|
|
68
|
+
const tis = useTis();
|
|
69
|
+
const [open, setOpen] = useState(false);
|
|
70
|
+
// Held across the dialog's lifetime rather than kept in state: it is
|
|
71
|
+
// never rendered, and re-rendering on every Start press would reopen
|
|
72
|
+
// the question of whether the action is stale.
|
|
73
|
+
const held = useRef<(() => void) | null>(null);
|
|
74
|
+
const blocked = tis.cycleConfigFields.length > 0 && !tis.cycleConfig.ready;
|
|
75
|
+
|
|
76
|
+
const run = useCallback((action: () => void) => {
|
|
77
|
+
const needsEntry = tis.cycleConfigFields.length > 0
|
|
78
|
+
&& (!tis.cycleConfig.ready || tis.cycleConfig.promptDue);
|
|
79
|
+
if (!needsEntry) { action(); return; }
|
|
80
|
+
held.current = action;
|
|
81
|
+
setOpen(true);
|
|
82
|
+
}, [tis.cycleConfigFields.length, tis.cycleConfig.ready, tis.cycleConfig.promptDue]);
|
|
83
|
+
|
|
84
|
+
const confirm = useCallback(() => {
|
|
85
|
+
const action = held.current;
|
|
86
|
+
held.current = null;
|
|
87
|
+
setOpen(false);
|
|
88
|
+
action?.();
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const cancel = useCallback(() => {
|
|
92
|
+
held.current = null;
|
|
93
|
+
setOpen(false);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return { open, setOpen, run, confirm, cancel, blocked };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export interface CycleConfigDialogProps {
|
|
100
|
+
/** Gate handle from [`useCycleConfigGate`]. Omit to let the dialog
|
|
101
|
+
* open itself on the server's `prompt_due` edge. */
|
|
102
|
+
controller?: CycleConfigGate;
|
|
103
|
+
/** Label on the confirm button. Default "Continue". Set it to
|
|
104
|
+
* "Start" when the dialog is gating a Start press. */
|
|
105
|
+
confirmLabel?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const CycleConfigDialog: React.FC<CycleConfigDialogProps> = ({
|
|
109
|
+
controller, confirmLabel = 'Continue',
|
|
110
|
+
}) => {
|
|
111
|
+
const tis = useTis();
|
|
112
|
+
const cfg = tis.cycleConfig;
|
|
113
|
+
const fields = tis.cycleConfigFields;
|
|
114
|
+
|
|
115
|
+
const [selfOpen, setSelfOpen] = useState(false);
|
|
116
|
+
const open = controller ? controller.open : selfOpen;
|
|
117
|
+
|
|
118
|
+
// Self-opening mode: follow the server's prompt_due edge. Rising edge
|
|
119
|
+
// only — an operator who dismisses the dialog isn't re-prompted until
|
|
120
|
+
// the next boundary, and the `tis.cycle_ready` gate still stops the
|
|
121
|
+
// machine in the meantime.
|
|
122
|
+
const prevPromptDue = useRef(false);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (controller) return;
|
|
125
|
+
const due = cfg.promptBeforeCycle && cfg.promptDue && fields.length > 0;
|
|
126
|
+
if (due && !prevPromptDue.current) setSelfOpen(true);
|
|
127
|
+
prevPromptDue.current = due;
|
|
128
|
+
}, [controller, cfg.promptBeforeCycle, cfg.promptDue, fields.length]);
|
|
129
|
+
|
|
130
|
+
// With a controller, confirming releases the held action (the Start
|
|
131
|
+
// press that was deferred). Self-opening, there's nothing to release.
|
|
132
|
+
const confirm = () => (controller ? controller.confirm() : setSelfOpen(false));
|
|
133
|
+
const cancel = () => (controller ? controller.cancel() : setSelfOpen(false));
|
|
134
|
+
|
|
135
|
+
const missingLabels = cfg.missing
|
|
136
|
+
.map(n => fields.find(f => f.name === n))
|
|
137
|
+
.filter((f): f is NonNullable<typeof f> => !!f)
|
|
138
|
+
.map(labelOf);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Dialog
|
|
142
|
+
header="Cycle Configuration"
|
|
143
|
+
visible={open}
|
|
144
|
+
onHide={cancel}
|
|
145
|
+
style={{ width: '34rem', maxWidth: '95vw' }}
|
|
146
|
+
modal
|
|
147
|
+
footer={
|
|
148
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '1rem' }}>
|
|
149
|
+
<span style={{ color: 'var(--text-secondary-color)', textAlign: 'left' }}>
|
|
150
|
+
{cfg.ready
|
|
151
|
+
? 'Recorded on the next cycle.'
|
|
152
|
+
: `Still required: ${missingLabels.join(', ')}`}
|
|
153
|
+
</span>
|
|
154
|
+
<span>
|
|
155
|
+
<Button label="Cancel" text onClick={cancel} />
|
|
156
|
+
<Button
|
|
157
|
+
label={confirmLabel}
|
|
158
|
+
icon="pi pi-check"
|
|
159
|
+
// The only place a disabled control is right:
|
|
160
|
+
// this button *is* the start, and starting with
|
|
161
|
+
// the entry incomplete is what we're preventing.
|
|
162
|
+
// The reason is spelled out beside it.
|
|
163
|
+
disabled={!cfg.ready}
|
|
164
|
+
onClick={confirm}
|
|
165
|
+
/>
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
<CycleConfigForm compact />
|
|
171
|
+
</Dialog>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <CycleConfigForm> — the Cycle tab. Operator-entered annotation that is
|
|
5
|
+
* recorded onto each cycle row: "which of the six positions is this?"
|
|
6
|
+
*
|
|
7
|
+
* Renders the selected method's `cycle_config_fields` with the same field
|
|
8
|
+
* rendering, unit handling and validation rules as Test Configuration (see
|
|
9
|
+
* TestFieldRow.tsx). What differs is the commit path and the lifecycle:
|
|
10
|
+
*
|
|
11
|
+
* - Every edit round-trips through `tis.set_cycle_config` immediately.
|
|
12
|
+
* There is no local draft: the value the operator sees must be the
|
|
13
|
+
* value the server would stamp on the next row, because that is what
|
|
14
|
+
* the `tis.cycle_ready` gate is computed from.
|
|
15
|
+
* - While a cycle is running its annotation is LATCHED. The fields stay
|
|
16
|
+
* visible but read-only, with a note saying edits apply to the next
|
|
17
|
+
* cycle — an edit that silently relabelled the row being measured
|
|
18
|
+
* would be worse than not being able to type at all.
|
|
19
|
+
* - Under `cycle` scope the values clear after each recorded row
|
|
20
|
+
* (`autoclear`), the gate re-arms, and the operator annotates again.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useContext, useState } from 'react';
|
|
24
|
+
import { Button } from 'primereact/button';
|
|
25
|
+
import { Dialog } from 'primereact/dialog';
|
|
26
|
+
import { Message } from 'primereact/message';
|
|
27
|
+
import { useTis } from './TisProvider';
|
|
28
|
+
import { TestFieldRow, labelOf, type TestFieldDef } from './TestFieldRow';
|
|
29
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
30
|
+
|
|
31
|
+
export interface CycleConfigFormProps {
|
|
32
|
+
/** Extra class names applied to the root element. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Inline style merged onto the root element. */
|
|
35
|
+
style?: React.CSSProperties;
|
|
36
|
+
/** Hide the explanatory header (used inside the prompt dialog, where
|
|
37
|
+
* the dialog title already says what this is). */
|
|
38
|
+
compact?: boolean;
|
|
39
|
+
/** Called after each accepted edit — the dialog uses it to know the
|
|
40
|
+
* operator has engaged with the form. */
|
|
41
|
+
onValueCommitted?: (field: TestFieldDef, raw: any) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One-line summary of what the next recorded row will be labelled with.
|
|
46
|
+
* Shared by the Cycle tab header and <CycleStatusStrip> so the two can't
|
|
47
|
+
* describe the same state differently.
|
|
48
|
+
*/
|
|
49
|
+
export const cycleConfigSummary = (
|
|
50
|
+
fields: TestFieldDef[],
|
|
51
|
+
values: Record<string, any>,
|
|
52
|
+
): string => {
|
|
53
|
+
if (fields.length === 0) return '';
|
|
54
|
+
return fields
|
|
55
|
+
.map(f => {
|
|
56
|
+
const v = values?.[f.name];
|
|
57
|
+
const shown = v === undefined || v === null || v === '' ? '—' : String(v);
|
|
58
|
+
return `${f.label ?? f.name}: ${shown}`;
|
|
59
|
+
})
|
|
60
|
+
.join(' ');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CycleConfigForm: React.FC<CycleConfigFormProps> = ({
|
|
64
|
+
className, style, compact, onValueCommitted,
|
|
65
|
+
}) => {
|
|
66
|
+
const tis = useTis();
|
|
67
|
+
const { write } = useContext(EventEmitterContext);
|
|
68
|
+
const cfg = tis.cycleConfig;
|
|
69
|
+
const fields = tis.cycleConfigFields;
|
|
70
|
+
const [infoField, setInfoField] = useState<TestFieldDef | null>(null);
|
|
71
|
+
|
|
72
|
+
// A latch exists exactly while a cycle is running against these values.
|
|
73
|
+
const locked = cfg.latched !== null;
|
|
74
|
+
|
|
75
|
+
const commit = async (field: TestFieldDef, raw: any) => {
|
|
76
|
+
await tis.setCycleConfigValues({ [field.name]: raw ?? null });
|
|
77
|
+
// A `source`-bound annotation also drives the machine (e.g. the
|
|
78
|
+
// position the fixture should move to), so mirror it to GM the way
|
|
79
|
+
// Test Configuration does. Storage on both sides stays RAW.
|
|
80
|
+
if (field.source) {
|
|
81
|
+
try { await write(field.source, raw); }
|
|
82
|
+
catch (e) { console.error('[CycleConfigForm] failed to write source:', e); }
|
|
83
|
+
}
|
|
84
|
+
onValueCommitted?.(field, raw);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (fields.length === 0) {
|
|
88
|
+
return (
|
|
89
|
+
<div className={className} style={{ padding: '1.25rem', ...style }}>
|
|
90
|
+
<h3 className="ac-form-section">No Cycle Configuration</h3>
|
|
91
|
+
<p style={{ color: 'var(--text-secondary-color)', marginTop: '0.5rem' }}>
|
|
92
|
+
This test method records no operator-entered columns. Cycle rows
|
|
93
|
+
are written entirely from measured values.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const gridStyle: React.CSSProperties = {
|
|
100
|
+
padding: '1.25rem',
|
|
101
|
+
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={className} style={style}>
|
|
106
|
+
{!compact && (
|
|
107
|
+
<div style={{ padding: '1.25rem 1.25rem 0' }}>
|
|
108
|
+
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
109
|
+
Cycle Configuration
|
|
110
|
+
{/* Status is stated in full by the messages below;
|
|
111
|
+
this is the at-a-glance marker beside the title. */}
|
|
112
|
+
<i
|
|
113
|
+
className={cfg.ready ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'}
|
|
114
|
+
style={{ color: cfg.ready ? 'var(--green-500)' : 'var(--red-500)' }}
|
|
115
|
+
aria-label={cfg.ready ? 'Cycle Configuration complete' : 'Cycle Configuration incomplete'}
|
|
116
|
+
/>
|
|
117
|
+
</h3>
|
|
118
|
+
<p style={{ color: 'var(--text-secondary-color)', margin: '0.25rem 0 0' }}>
|
|
119
|
+
{cfg.scope === 'cycle'
|
|
120
|
+
? 'Recorded on the next cycle. Entered again for every cycle.'
|
|
121
|
+
: 'Recorded on every cycle of this run.'}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{locked && (
|
|
127
|
+
<div style={{ padding: '0.75rem 1.25rem 0' }}>
|
|
128
|
+
<Message
|
|
129
|
+
severity="info"
|
|
130
|
+
text="A cycle is running with these values. Edits apply to the next cycle."
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{!cfg.ready && !locked && (
|
|
136
|
+
<div style={{ padding: '0.75rem 1.25rem 0' }}>
|
|
137
|
+
<Message
|
|
138
|
+
severity="warn"
|
|
139
|
+
text={
|
|
140
|
+
'Required: ' +
|
|
141
|
+
cfg.missing
|
|
142
|
+
.map(n => fields.find(f => f.name === n))
|
|
143
|
+
.map((f, i) => (f ? labelOf(f) : cfg.missing[i]))
|
|
144
|
+
.join(', ') +
|
|
145
|
+
'. The machine will not start until these are filled in.'
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
<div className="ac-form-grid" style={gridStyle}>
|
|
152
|
+
{fields.map(field => (
|
|
153
|
+
<TestFieldRow
|
|
154
|
+
key={field.name}
|
|
155
|
+
field={field}
|
|
156
|
+
// While latched, show what the running cycle is
|
|
157
|
+
// actually recording rather than the pending entry.
|
|
158
|
+
value={locked ? cfg.latched?.[field.name] : cfg.values?.[field.name]}
|
|
159
|
+
disabled={locked}
|
|
160
|
+
onChange={(raw) => { void commit(field, raw); }}
|
|
161
|
+
onInfo={(f) => setInfoField(f)}
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{!compact && (
|
|
167
|
+
<div style={{ padding: '0 1.25rem 1.25rem' }}>
|
|
168
|
+
<Button
|
|
169
|
+
label="Clear"
|
|
170
|
+
icon="pi pi-eraser"
|
|
171
|
+
text
|
|
172
|
+
disabled={locked}
|
|
173
|
+
onClick={() => { void tis.clearCycleConfig(); }}
|
|
174
|
+
tooltip="Clear every entry, including fields that don't autoclear"
|
|
175
|
+
tooltipOptions={{ position: 'top' }}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<Dialog
|
|
181
|
+
header={infoField ? labelOf(infoField) : ''}
|
|
182
|
+
visible={infoField !== null}
|
|
183
|
+
onHide={() => setInfoField(null)}
|
|
184
|
+
style={{ width: '32rem', maxWidth: '90vw' }}
|
|
185
|
+
modal
|
|
186
|
+
dismissableMask
|
|
187
|
+
>
|
|
188
|
+
<p style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{infoField?.description}</p>
|
|
189
|
+
</Dialog>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <CycleStatusStrip> — the Cycle Configuration state, rendered where the
|
|
5
|
+
* operator is already looking: next to the Start button.
|
|
6
|
+
*
|
|
7
|
+
* The Cycle tab is three panes away from the run screen. An operator
|
|
8
|
+
* about to press Start is looking at the lower-right corner of the
|
|
9
|
+
* footer, not at a tab they may never open, so the annotation that is
|
|
10
|
+
* about to be written onto the row has to be visible from there. This is
|
|
11
|
+
* the same idea as the specimen number Bluehill keeps beside its run
|
|
12
|
+
* control.
|
|
13
|
+
*
|
|
14
|
+
* Drop it into the footer beside Start:
|
|
15
|
+
*
|
|
16
|
+
* <div className="footer">
|
|
17
|
+
* <CycleStatusStrip />
|
|
18
|
+
* <Button label="Start" onClick={...} />
|
|
19
|
+
* </div>
|
|
20
|
+
*
|
|
21
|
+
* Clicking it opens the entry form in a dialog, so the operator can fill
|
|
22
|
+
* the fields in without leaving the run screen.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React, { useState } from 'react';
|
|
26
|
+
import { Button } from 'primereact/button';
|
|
27
|
+
import { Dialog } from 'primereact/dialog';
|
|
28
|
+
import { useTis } from './TisProvider';
|
|
29
|
+
import { CycleConfigForm, cycleConfigSummary } from './CycleConfigForm';
|
|
30
|
+
|
|
31
|
+
export interface CycleStatusStripProps {
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: React.CSSProperties;
|
|
34
|
+
/** Render nothing when the method declares no annotation columns.
|
|
35
|
+
* Default true — an empty chip beside Start is just noise. */
|
|
36
|
+
hideWhenUnused?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CycleStatusStrip: React.FC<CycleStatusStripProps> = ({
|
|
40
|
+
className, style, hideWhenUnused = true,
|
|
41
|
+
}) => {
|
|
42
|
+
const tis = useTis();
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const cfg = tis.cycleConfig;
|
|
45
|
+
const fields = tis.cycleConfigFields;
|
|
46
|
+
|
|
47
|
+
if (fields.length === 0 && hideWhenUnused) return null;
|
|
48
|
+
|
|
49
|
+
// Latched values are what the running cycle will actually record;
|
|
50
|
+
// pending values are what the next one will get.
|
|
51
|
+
const shown = cfg.latched ?? cfg.values;
|
|
52
|
+
const summary = cycleConfigSummary(fields, shown);
|
|
53
|
+
const incomplete = !cfg.ready;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Button
|
|
58
|
+
className={className}
|
|
59
|
+
style={style}
|
|
60
|
+
text
|
|
61
|
+
severity={incomplete ? 'danger' : undefined}
|
|
62
|
+
icon={incomplete ? 'pi pi-exclamation-triangle' : 'pi pi-tag'}
|
|
63
|
+
// Not disabled and not silent: the operator can always open
|
|
64
|
+
// it, and when something is missing the chip says so in
|
|
65
|
+
// words rather than making them hunt for a red field.
|
|
66
|
+
label={incomplete
|
|
67
|
+
? `${summary} — entry required`
|
|
68
|
+
: summary}
|
|
69
|
+
aria-label="Cycle Configuration"
|
|
70
|
+
onClick={() => setOpen(true)}
|
|
71
|
+
/>
|
|
72
|
+
<Dialog
|
|
73
|
+
header="Cycle Configuration"
|
|
74
|
+
visible={open}
|
|
75
|
+
onHide={() => setOpen(false)}
|
|
76
|
+
style={{ width: '34rem', maxWidth: '95vw' }}
|
|
77
|
+
modal
|
|
78
|
+
dismissableMask
|
|
79
|
+
>
|
|
80
|
+
<CycleConfigForm />
|
|
81
|
+
</Dialog>
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -169,6 +169,10 @@ export interface TestMethod {
|
|
|
169
169
|
* optional so the compiler demands a `?? []` at every use. */
|
|
170
170
|
project_fields?: TestFieldDef[];
|
|
171
171
|
config_fields?: TestFieldDef[];
|
|
172
|
+
/** Operator-entered per-row annotation. Recorded onto each cycle row
|
|
173
|
+
* by the server and rendered as the first Cycle Data columns after
|
|
174
|
+
* the row index, in declared order. */
|
|
175
|
+
cycle_config_fields?: TestFieldDef[];
|
|
172
176
|
cycle_fields?: TestFieldDef[];
|
|
173
177
|
results_fields?: TestFieldDef[];
|
|
174
178
|
raw_data?: RawDataShape | null;
|
|
@@ -799,13 +803,25 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
799
803
|
// valid and arrives with the key absent. Without the guard the
|
|
800
804
|
// `.map` throws inside render and React unmounts the whole
|
|
801
805
|
// app — the operator sees a blank page, not a missing table.
|
|
802
|
-
|
|
806
|
+
// Operator-entered annotation leads, right after the row
|
|
807
|
+
// index, then the measured columns — the same order the
|
|
808
|
+
// server writes into every CSV export. Annotation columns
|
|
809
|
+
// are always editable: they're the operator's own note, so
|
|
810
|
+
// correcting a mistyped one is the intended workflow (the
|
|
811
|
+
// server accepts cycle_config_fields on tis.update_cycle
|
|
812
|
+
// without an `editable` flag), while measured columns still
|
|
813
|
+
// have to opt in.
|
|
814
|
+
const toColumn = (f: TestFieldDef, editable: boolean): ScienceColumn => ({
|
|
803
815
|
field: f.name,
|
|
804
816
|
label: f.name,
|
|
805
817
|
units: f.units,
|
|
806
|
-
editable
|
|
818
|
+
editable,
|
|
807
819
|
body: (row) => formatCell(row[f.name], f.type, f.scale),
|
|
808
|
-
})
|
|
820
|
+
});
|
|
821
|
+
const cycleColumns: ScienceColumn[] = [
|
|
822
|
+
...(schema.cycle_config_fields ?? []).map(f => toColumn(f, true)),
|
|
823
|
+
...(schema.cycle_fields ?? []).map(f => toColumn(f, !!f.editable)),
|
|
824
|
+
];
|
|
809
825
|
return (
|
|
810
826
|
<ScienceTable
|
|
811
827
|
title={`Cycle Data (${cycles.length})`}
|