@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.
Files changed (62) hide show
  1. package/dist/components/index.d.ts +10 -2
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis/CycleConfigDialog.d.ts +39 -0
  5. package/dist/components/tis/CycleConfigDialog.d.ts.map +1 -0
  6. package/dist/components/tis/CycleConfigDialog.js +1 -0
  7. package/dist/components/tis/CycleConfigForm.d.ts +22 -0
  8. package/dist/components/tis/CycleConfigForm.d.ts.map +1 -0
  9. package/dist/components/tis/CycleConfigForm.js +1 -0
  10. package/dist/components/tis/CycleStatusStrip.d.ts +10 -0
  11. package/dist/components/tis/CycleStatusStrip.d.ts.map +1 -0
  12. package/dist/components/tis/CycleStatusStrip.js +1 -0
  13. package/dist/components/tis/TestDataView.d.ts +4 -0
  14. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  15. package/dist/components/tis/TestDataView.js +1 -1
  16. package/dist/components/tis/TestFieldRow.d.ts +151 -0
  17. package/dist/components/tis/TestFieldRow.d.ts.map +1 -0
  18. package/dist/components/tis/TestFieldRow.js +1 -0
  19. package/dist/components/tis/TestSetupForm.d.ts +15 -51
  20. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  21. package/dist/components/tis/TestSetupForm.js +1 -1
  22. package/dist/components/tis/TestSetupView.d.ts.map +1 -1
  23. package/dist/components/tis/TestSetupView.js +1 -1
  24. package/dist/components/tis/TisProvider.d.ts +80 -0
  25. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  26. package/dist/components/tis/TisProvider.js +1 -1
  27. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  28. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  29. package/dist/components/tis-editor/editor/CycleConfigSection.d.ts +19 -0
  30. package/dist/components/tis-editor/editor/CycleConfigSection.d.ts.map +1 -0
  31. package/dist/components/tis-editor/editor/CycleConfigSection.js +1 -0
  32. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  33. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  34. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
  35. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
  36. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +3 -0
  37. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  38. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  39. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  40. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  41. package/dist/components/tis-editor/types.d.ts +28 -1
  42. package/dist/components/tis-editor/types.d.ts.map +1 -1
  43. package/dist/components/tis-editor/validation.d.ts.map +1 -1
  44. package/dist/components/tis-editor/validation.js +1 -1
  45. package/package.json +1 -1
  46. package/src/components/index.ts +14 -0
  47. package/src/components/tis/CycleConfigDialog.tsx +173 -0
  48. package/src/components/tis/CycleConfigForm.tsx +192 -0
  49. package/src/components/tis/CycleStatusStrip.tsx +84 -0
  50. package/src/components/tis/TestDataView.tsx +19 -3
  51. package/src/components/tis/TestFieldRow.tsx +297 -0
  52. package/src/components/tis/TestSetupForm.tsx +52 -212
  53. package/src/components/tis/TestSetupView.tsx +60 -3
  54. package/src/components/tis/TisProvider.tsx +191 -0
  55. package/src/components/tis-editor/TisConfigEditor.tsx +1 -0
  56. package/src/components/tis-editor/editor/CycleConfigSection.tsx +93 -0
  57. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +10 -0
  58. package/src/components/tis-editor/editor/MethodFormEditor.tsx +10 -4
  59. package/src/components/tis-editor/editor/TestFieldDialog.tsx +22 -2
  60. package/src/components/tis-editor/editor/ViewsEditor.tsx +3 -0
  61. package/src/components/tis-editor/types.ts +34 -1
  62. package/src/components/tis-editor/validation.ts +20 -2
@@ -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
- const cycleColumns: ScienceColumn[] = (schema.cycle_fields ?? []).map(f => ({
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: f.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})`}