@adcops/autocore-react 3.5.7 → 3.5.11

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 (95) hide show
  1. package/dist/components/forms/useEditDraft.d.ts +49 -0
  2. package/dist/components/forms/useEditDraft.d.ts.map +1 -0
  3. package/dist/components/forms/useEditDraft.js +1 -0
  4. package/dist/components/index.d.ts +6 -0
  5. package/dist/components/index.d.ts.map +1 -1
  6. package/dist/components/index.js +1 -1
  7. package/dist/components/tis/TestDataView.d.ts +8 -0
  8. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  9. package/dist/components/tis/TestDataView.js +1 -1
  10. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  11. package/dist/components/tis/TestRawDataView.js +1 -1
  12. package/dist/components/tis/chartViews.d.ts +57 -0
  13. package/dist/components/tis/chartViews.d.ts.map +1 -0
  14. package/dist/components/tis/chartViews.js +1 -0
  15. package/dist/components/tis-editor/TisConfigEditor.css +11 -0
  16. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  17. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  18. package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
  19. package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
  20. package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
  21. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
  22. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
  23. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  24. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  25. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  26. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
  27. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
  28. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
  29. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  30. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  31. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  32. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  33. package/dist/components/tis-editor/types.d.ts +6 -0
  34. package/dist/components/tis-editor/types.d.ts.map +1 -1
  35. package/dist/components/varpick/VariableInput.d.ts +41 -0
  36. package/dist/components/varpick/VariableInput.d.ts.map +1 -0
  37. package/dist/components/varpick/VariableInput.js +1 -0
  38. package/dist/components/varpick/VariablePicker.d.ts +45 -0
  39. package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
  40. package/dist/components/varpick/VariablePicker.js +1 -0
  41. package/dist/components/varpick/index.d.ts +16 -0
  42. package/dist/components/varpick/index.d.ts.map +1 -0
  43. package/dist/components/varpick/index.js +1 -0
  44. package/dist/components/varpick/tisCatalogs.d.ts +85 -0
  45. package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
  46. package/dist/components/varpick/tisCatalogs.js +1 -0
  47. package/dist/components/varpick/varpick.css +167 -0
  48. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  49. package/dist/core/AutoCoreTagContext.js +1 -1
  50. package/dist/core/AutoCoreTagTypes.d.ts +13 -0
  51. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  52. package/dist/hooks/index.d.ts +4 -0
  53. package/dist/hooks/index.d.ts.map +1 -1
  54. package/dist/hooks/index.js +1 -1
  55. package/dist/hooks/useGmVariables.d.ts +57 -0
  56. package/dist/hooks/useGmVariables.d.ts.map +1 -0
  57. package/dist/hooks/useGmVariables.js +1 -0
  58. package/dist/hooks/useSequenceProducers.d.ts +38 -0
  59. package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
  60. package/dist/hooks/useSequenceProducers.js +1 -0
  61. package/package.json +7 -2
  62. package/src/components/forms/useEditDraft.ts +0 -0
  63. package/src/components/index.ts +20 -0
  64. package/src/components/tis/TestDataView.tsx +16 -10
  65. package/src/components/tis/TestRawDataView.tsx +6 -7
  66. package/src/components/tis/chartViews.ts +104 -0
  67. package/src/components/tis-editor/TisConfigEditor.css +11 -0
  68. package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
  69. package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
  70. package/src/components/tis-editor/editor/ChartViewDialog.tsx +48 -17
  71. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
  72. package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
  73. package/src/components/tis-editor/editor/TestFieldDialog.tsx +90 -23
  74. package/src/components/tis-editor/editor/ViewsEditor.tsx +56 -5
  75. package/src/components/tis-editor/types.ts +6 -0
  76. package/src/components/varpick/VariableInput.tsx +126 -0
  77. package/src/components/varpick/VariablePicker.tsx +168 -0
  78. package/src/components/varpick/index.ts +22 -0
  79. package/src/components/varpick/tisCatalogs.ts +235 -0
  80. package/src/components/varpick/varpick.css +167 -0
  81. package/src/core/AutoCoreTagContext.tsx +3 -1
  82. package/src/core/AutoCoreTagTypes.ts +14 -0
  83. package/src/hooks/index.ts +11 -0
  84. package/src/hooks/useGmVariables.ts +142 -0
  85. package/src/hooks/useSequenceProducers.ts +104 -0
  86. package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
  87. package/tools/tests/dist/autocore-react.css +257 -0
  88. package/tools/tests/dist/tis-editor.entry.js +9295 -0
  89. package/tools/tests/dist/varpick.entry.js +113 -0
  90. package/tools/tests/live-server.probe.mjs +172 -0
  91. package/tools/tests/tis-editor.entry.tsx +46 -0
  92. package/tools/tests/tis-editor.test.mjs +133 -0
  93. package/tools/tests/varpick.entry.tsx +81 -0
  94. package/tools/tests/varpick.test.mjs +208 -0
  95. package/tools/tests/vite.config.mjs +35 -0
@@ -4,6 +4,7 @@ import { Checkbox } from 'primereact/checkbox';
4
4
  import { FormSection } from '../../forms/FormSection';
5
5
  import { FormRow } from '../../forms/FormRow';
6
6
  import type { RawColumn, RawDataShape, TestMethod } from '../types';
7
+ import { VariableInput, rawSourceCatalog } from '../../varpick';
7
8
 
8
9
  export interface RawDataEditorProps {
9
10
  method: TestMethod;
@@ -89,10 +90,14 @@ export const RawDataEditor: React.FC<RawDataEditorProps> = ({ method, onChange }
89
90
  defaultValue={name}
90
91
  onBlur={(e) => renameColumn(name, e.target.value.trim())}
91
92
  />
92
- <InputText
93
+ <VariableInput
93
94
  placeholder="source (time / ni.<daq>.channels.<name> / derived)"
94
95
  value={col.source}
95
- onChange={(e) => updateColumn(name, { ...col, source: e.target.value })}
96
+ onChange={(v) => updateColumn(name, { ...col, source: v })}
97
+ catalog={() => rawSourceCatalog()}
98
+ ariaLabel={`Choose the source for column ${name}`}
99
+ pickerTitle="Column source"
100
+ pickerHint="A DAQ channel is named ni.<daq>.channels.<name> and is not listed here — the editor has no channel catalog, so type it."
96
101
  />
97
102
  <InputText
98
103
  placeholder="formula (only when source=derived)"
@@ -7,11 +7,15 @@ import { Dropdown } from 'primereact/dropdown';
7
7
  import { Checkbox } from 'primereact/checkbox';
8
8
  import { InputNumber } from 'primereact/inputnumber';
9
9
  import { FormRow } from '../../forms/FormRow';
10
+ import { useEditDraft } from '../../forms/useEditDraft';
10
11
  import type { TestField, AggFn, AggFrom, AggScope, AggregateSpec } from '../types';
11
12
  // One home for bound parsing/formatting — shared with the operator-facing
12
13
  // form so the editor and the runtime can never disagree on what a bound is.
13
14
  import { boundText, parseBound, boundInvalid } from '../../tis/TestFieldRow';
14
15
  import { AutoCoreTagContext } from '../../../core/AutoCoreTagContext';
16
+ import { VariableInput } from '../../varpick';
17
+ import { boundCatalog, configSourceCatalog, cycleSourceCatalog } from '../../varpick';
18
+ import { useAuthoringCatalogs } from './AuthoringContext';
15
19
 
16
20
 
17
21
  const FIELD_TYPES: { label: string; value: string }[] = [
@@ -73,20 +77,50 @@ export interface TestFieldDialogProps {
73
77
  /** Candidate target field names for an aggregate's `of`, by source. */
74
78
  cycleFieldNames?: string[];
75
79
  resultsFieldNames?: string[];
80
+ /**
81
+ * True when this field belongs to `cycle_fields`. Only there does Source
82
+ * have two possible producers — a GM tag read at record time, or the
83
+ * sequence storing a value under this column's NAME — so only there is the
84
+ * picker a producer picker rather than a variable list.
85
+ */
86
+ isCycleField?: boolean;
76
87
  }
77
88
 
78
89
  const blank: TestField = { name: '', type: 'f32' };
79
90
 
80
91
  export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
81
92
  visible, initial, onCancel, onSave, siblingNames,
82
- isResultsField = false, isCycleConfigField = false,
93
+ isResultsField = false, isCycleConfigField = false, isCycleField = false,
83
94
  cycleFieldNames = [], resultsFieldNames = [],
84
95
  }) => {
85
- const [draft, setDraft] = useState<TestField>(blank);
86
- // `default` is edited as free text so a numeric literal, a string, or an
87
- // `${fqdn}` token can all be entered in one field; it's coerced on save.
88
- const [defaultText, setDefaultText] = useState<string>('');
96
+ // Supplied by the editor above; see AuthoringContext for why this is not
97
+ // four levels of props.
98
+ const { variables, producedNames } = useAuthoringCatalogs();
99
+ // The field under edit plus its free-text Default, as one draft seeded
100
+ // once per open. `default` is edited as free text so a numeric literal, a
101
+ // string, or an `${fqdn}` token can all be entered in one field; it's
102
+ // coerced on save.
103
+ //
104
+ // Keyed on the field NAME, not on the `initial` object: this editor
105
+ // re-renders with the live tag traffic underneath it, and an
106
+ // identity-keyed reseed discards the operator's typing (see
107
+ // useEditDraft).
108
+ const { draft: form, setDraft: setForm, token } = useEditDraft(
109
+ visible,
110
+ initial?.name ?? '',
111
+ () => ({
112
+ field: initial ? { ...initial } : { ...blank },
113
+ defaultText: initial?.default == null ? '' : String(initial.default),
114
+ }),
115
+ );
116
+ const draft = form.field;
117
+ const defaultText = form.defaultText;
118
+ const setDraft = (field: TestField) => setForm(f => ({ ...f, field }));
119
+ const setDefaultText = (defaultText: string) => setForm(f => ({ ...f, defaultText }));
120
+
89
121
  const [error, setError] = useState<string | null>(null);
122
+ // Clears on open and on a switch to another field, and on no other render.
123
+ useEffect(() => { setError(null); }, [token]);
90
124
 
91
125
  // The quantity names this machine actually declares, from the resolved
92
126
  // units table. Offered as a dropdown so an author picks a row that exists
@@ -99,13 +133,23 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
99
133
  [scales],
100
134
  );
101
135
 
102
- useEffect(() => {
103
- if (visible) {
104
- setDraft(initial ? { ...initial } : { ...blank });
105
- setDefaultText(initial?.default == null ? '' : String(initial.default));
106
- setError(null);
107
- }
108
- }, [visible, initial]);
136
+ // What the pickers offer. Units come from the machine's resolved scales, so
137
+ // an entry is labelled in the operator's active system rather than in
138
+ // whatever the variable's backend unit happens to be.
139
+ const catalogCtx = useMemo(
140
+ () => ({
141
+ variables,
142
+ unitFor: (q: string) => scales?.[q]?.label,
143
+ }),
144
+ [variables, scales],
145
+ );
146
+
147
+ // A cycle field's Source is the one that needs BOTH producers offered; every
148
+ // other array only ever binds a GM tag the setup form writes.
149
+ const sourceCatalog = () =>
150
+ isCycleField
151
+ ? cycleSourceCatalog(catalogCtx, { producedNames, fieldName: draft.name })
152
+ : configSourceCatalog(catalogCtx);
109
153
 
110
154
  // Turn the free-text Default into its stored form: an `${fqdn}` token and
111
155
  // other non-numerics stay strings; numeric/bool literals are stored typed.
@@ -262,10 +306,21 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
262
306
  />
263
307
  </FormRow>
264
308
  )}
265
- <FormRow label="Source" hint="Optional gm.* variable to bind this field to.">
266
- <InputText
309
+ <FormRow
310
+ label="Source"
311
+ hint={isCycleField
312
+ ? 'Where this column\u2019s value comes from. Leave empty when the sequence records it \u2014 a store_value step is matched to this column by NAME, not by a binding.'
313
+ : 'Optional gm.* variable this field reads and writes.'}
314
+ >
315
+ <VariableInput
267
316
  value={draft.source ?? ''}
268
- onChange={(e) => setDraft({ ...draft, source: e.target.value || undefined })}
317
+ onChange={(v) => setDraft({ ...draft, source: v || undefined })}
318
+ catalog={sourceCatalog}
319
+ ariaLabel="Choose a source for this field"
320
+ pickerTitle="Where does this value come from?"
321
+ pickerHint={isCycleField
322
+ ? 'A column filled by the sequence needs no Source \u2014 choosing one below clears it, which is correct.'
323
+ : 'The Test Setup form writes the operator\u2019s entry to this variable, so only variables a UI may write are listed.'}
269
324
  placeholder="gm.<variable_name>"
270
325
  />
271
326
  </FormRow>
@@ -284,10 +339,14 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
284
339
  label="Min"
285
340
  hint="Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} — for a bound that moves, e.g. a load-cell force ceiling."
286
341
  >
287
- <InputText
342
+ <VariableInput
288
343
  value={boundText(draft.min)}
289
- onChange={(e) => setDraft({ ...draft, min: parseBound(e.target.value) })}
290
- className={boundInvalid(draft.min) ? 'p-invalid' : ''}
344
+ onChange={(v) => setDraft({ ...draft, min: parseBound(v) })}
345
+ invalid={boundInvalid(draft.min)}
346
+ catalog={() => boundCatalog(catalogCtx)}
347
+ ariaLabel="Bind Min to a machine variable"
348
+ pickerTitle="Bind Min to a machine value"
349
+ pickerHint="The bound then follows the machine — a load-cell ceiling that changes with the fitted cell, rather than a number that goes stale."
291
350
  placeholder="number or ${gm.…}"
292
351
  />
293
352
  </FormRow>
@@ -295,17 +354,25 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
295
354
  label="Max"
296
355
  hint="Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}."
297
356
  >
298
- <InputText
357
+ <VariableInput
299
358
  value={boundText(draft.max)}
300
- onChange={(e) => setDraft({ ...draft, max: parseBound(e.target.value) })}
301
- className={boundInvalid(draft.max) ? 'p-invalid' : ''}
359
+ onChange={(v) => setDraft({ ...draft, max: parseBound(v) })}
360
+ invalid={boundInvalid(draft.max)}
361
+ catalog={() => boundCatalog(catalogCtx)}
362
+ ariaLabel="Bind Max to a machine variable"
363
+ pickerTitle="Bind Max to a machine value"
364
+ pickerHint="The bound then follows the machine — a load-cell ceiling that changes with the fitted cell, rather than a number that goes stale."
302
365
  placeholder="number or ${gm.…}"
303
366
  />
304
367
  </FormRow>
305
368
  <FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
306
- <InputText
369
+ <VariableInput
307
370
  value={defaultText}
308
- onChange={(e) => setDefaultText(e.target.value)}
371
+ onChange={setDefaultText}
372
+ catalog={() => boundCatalog(catalogCtx)}
373
+ ariaLabel="Use a machine variable as the default"
374
+ pickerTitle="Default from a machine value"
375
+ pickerHint="A token default SNAPSHOTS the value when the method loads. It is not a live binding — that is what Source is."
309
376
  placeholder="e.g. 5 or ${gm.safe_speed}"
310
377
  />
311
378
  </FormRow>
@@ -3,8 +3,11 @@ import { DataTable } from 'primereact/datatable';
3
3
  import { Column } from 'primereact/column';
4
4
  import { Button } from 'primereact/button';
5
5
  import { FormSection } from '../../forms/FormSection';
6
+ import { moveView, orderedViews } from '../../tis/chartViews';
6
7
  import { ChartViewDialog } from './ChartViewDialog';
7
8
  import type { ChartView, TestField, TestMethod } from '../types';
9
+ import { chartFieldCatalog } from '../../varpick';
10
+ import type { Catalog } from '@adcops/autocore-varpick';
8
11
 
9
12
  export interface ViewsEditorProps {
10
13
  method: TestMethod;
@@ -19,8 +22,11 @@ interface ViewRow {
19
22
 
20
23
  export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) => {
21
24
  const views = (method.views as Record<string, ChartView>) ?? {};
22
- const rows: ViewRow[] = Object.entries(views).map(([id, v]) => ({
23
- id, title: v.title ?? '', type: v.type ?? '',
25
+ // Display order, not id order the operator sets it with the arrows and
26
+ // the runtime reads the same `order` key. The first row is the view the
27
+ // Data tab opens on.
28
+ const rows: ViewRow[] = orderedViews(views).map(({ name, view }) => ({
29
+ id: name, title: view.title ?? '', type: view.type ?? '',
24
30
  }));
25
31
 
26
32
  const [dialogOpen, setDialogOpen] = useState(false);
@@ -43,6 +49,28 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
43
49
  'timestamp',
44
50
  ];
45
51
 
52
+ // The same names, grouped and described, for the picker button. Built from
53
+ // `method` rather than from `knownKeys` so each group knows what it IS —
54
+ // a flat list cannot say that `cycle_index` is per-cycle and `operator` is
55
+ // per-test, which is the distinction that makes a chart plot a straight
56
+ // line by accident.
57
+ const fieldCatalog = (viewType: string): Catalog => chartFieldCatalog({
58
+ viewType,
59
+ cycleFields: [
60
+ ...((method.cycle_config_fields ?? []) as TestField[]).map(f => f.name),
61
+ ...((method.cycle_fields ?? []) as TestField[]).map(f => f.name),
62
+ // Written onto every row by the server, declared nowhere.
63
+ 'cycle_index',
64
+ 'timestamp',
65
+ ].filter(Boolean),
66
+ resultsFields: ((method.results_fields ?? []) as TestField[]).map(f => f.name).filter(Boolean),
67
+ rawColumns: Object.keys((method.raw_data as any)?.columns ?? {}),
68
+ testFields: [
69
+ ...((method.project_fields ?? []) as TestField[]).map(f => f.name),
70
+ ...((method.config_fields ?? []) as TestField[]).map(f => f.name),
71
+ ].filter(Boolean),
72
+ });
73
+
46
74
  const openNew = () => { setEditingId(null); setDialogOpen(true); };
47
75
  const openEdit = (id: string) => { setEditingId(id); setDialogOpen(true); };
48
76
 
@@ -57,6 +85,14 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
57
85
  setDialogOpen(false);
58
86
  };
59
87
 
88
+ // Move up / down renumbers `order` densely across every view, so the next
89
+ // move is a swap of adjacent integers whatever the method started as.
90
+ const handleMove = (idx: number, dir: -1 | 1) => {
91
+ const next = moveView(views, idx, dir);
92
+ if (next === views) return;
93
+ onChange({ ...method, views: next });
94
+ };
95
+
60
96
  const handleRemove = (id: string) => {
61
97
  if (!window.confirm(`Remove view "${id}"?`)) return;
62
98
  const next = { ...views };
@@ -64,8 +100,22 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
64
100
  onChange({ ...method, views: next });
65
101
  };
66
102
 
67
- const rowActions = (r: ViewRow) => (
103
+ const rowActions = (r: ViewRow, opts: { rowIndex: number }) => (
68
104
  <div style={{ display: 'flex', gap: '0.25rem' }}>
105
+ <Button
106
+ icon="pi pi-arrow-up"
107
+ className="p-button-text p-button-sm"
108
+ disabled={opts.rowIndex === 0}
109
+ onClick={() => handleMove(opts.rowIndex, -1)}
110
+ aria-label="Move up"
111
+ />
112
+ <Button
113
+ icon="pi pi-arrow-down"
114
+ className="p-button-text p-button-sm"
115
+ disabled={opts.rowIndex === rows.length - 1}
116
+ onClick={() => handleMove(opts.rowIndex, 1)}
117
+ aria-label="Move down"
118
+ />
69
119
  <Button
70
120
  icon="pi pi-pencil"
71
121
  className="p-button-text p-button-sm"
@@ -85,20 +135,21 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
85
135
  <>
86
136
  <FormSection
87
137
  title="Views"
88
- description="Named chart definitions rendered by <TestDataView> (cycle_scatter) and <TestRawDataView> (raw_trace)."
138
+ description="Named chart definitions rendered by <TestDataView> (cycle_scatter) and <TestRawDataView> (raw_trace). Top to bottom is the order the operator sees in the view picker, and the first one is the chart the Data tab opens on."
89
139
  actions={<Button label="Add view" icon="pi pi-plus" size="small" onClick={openNew} />}
90
140
  >
91
141
  <DataTable value={rows} dataKey="id" emptyMessage="No views defined.">
92
142
  <Column field="id" header="View ID" />
93
143
  <Column field="title" header="Title" />
94
144
  <Column field="type" header="Type" style={{ width: '8rem' }} />
95
- <Column header="" body={rowActions} style={{ width: '6rem' }} />
145
+ <Column header="" body={rowActions} style={{ width: '10rem' }} />
96
146
  </DataTable>
97
147
  </FormSection>
98
148
  <ChartViewDialog
99
149
  visible={dialogOpen}
100
150
  initial={editingId ? { viewId: editingId, view: views[editingId] } : null}
101
151
  knownKeys={knownKeys}
152
+ fieldCatalog={fieldCatalog}
102
153
  siblingIds={Object.keys(views)}
103
154
  onCancel={() => setDialogOpen(false)}
104
155
  onSave={handleSave}
@@ -104,6 +104,12 @@ export interface ChartView {
104
104
  * prints 17-digit ticks.
105
105
  */
106
106
  x_decimals?: number;
107
+ /**
108
+ * Position in the view picker, ascending. Set by the editor's Move
109
+ * up / Move down; see `components/tis/chartViews.ts` for why the order
110
+ * cannot just be the order of the keys.
111
+ */
112
+ order?: number;
107
113
  }
108
114
 
109
115
  export type RawColumnSource = 'time' | 'derived' | string; // also `ni.<daq>.channels.<name>`
@@ -0,0 +1,126 @@
1
+ /**
2
+ * VariableInput — a text input with a picker button.
3
+ *
4
+ * A drop-in replacement for the `<InputText>` inside a `FormRow`, so adding a
5
+ * picker to a field is a one-line change and the field keeps its label, hint and
6
+ * error handling.
7
+ *
8
+ * Typing still works exactly as before. That is the point: an author who knows
9
+ * the name types it, and an author who does not gets a list instead of a guess.
10
+ */
11
+
12
+ import * as React from 'react';
13
+ import { Button } from 'primereact/button';
14
+ import { InputText } from 'primereact/inputtext';
15
+ import {
16
+ applyInsert,
17
+ queryFromCaret,
18
+ type Catalog,
19
+ type InsertMode,
20
+ type VariableEntry,
21
+ } from '@adcops/autocore-varpick';
22
+ import { VariablePicker } from './VariablePicker';
23
+ import './varpick.css';
24
+
25
+ export interface VariableInputProps {
26
+ value: string;
27
+ onChange: (value: string) => void;
28
+ /**
29
+ * Built when the dialog opens, not on every render — a machine's variable
30
+ * table is ~100 rows and this sits inside a dialog that re-renders on live
31
+ * tag traffic.
32
+ */
33
+ catalog: () => Catalog;
34
+ /**
35
+ * `replace` when the whole field IS one binding (a `source`, a bound);
36
+ * `caret` when the field is an expression and the pick is one term of it.
37
+ * Defaults to `replace`, which is what every TIS field is.
38
+ */
39
+ mode?: InsertMode;
40
+ /** Dialog heading and the line under it. */
41
+ pickerTitle?: string;
42
+ pickerHint?: React.ReactNode;
43
+ /** Accessible name for the button — it is icon-only, so this is required. */
44
+ ariaLabel: string;
45
+ placeholder?: string;
46
+ className?: string;
47
+ disabled?: boolean;
48
+ invalid?: boolean;
49
+ }
50
+
51
+ export const VariableInput: React.FC<VariableInputProps> = ({
52
+ value, onChange, catalog, mode = 'replace', pickerTitle, pickerHint,
53
+ ariaLabel, placeholder, className, disabled, invalid,
54
+ }) => {
55
+ const inputRef = React.useRef<HTMLInputElement>(null);
56
+ const caret = React.useRef<{ start: number; end: number }>({ start: 0, end: 0 });
57
+ const [open, setOpen] = React.useState(false);
58
+ const [built, setBuilt] = React.useState<Catalog>([]);
59
+
60
+ // Captured on every selection change rather than read when the button is
61
+ // clicked: clicking blurs the input, and a blur can reset `selectionStart`
62
+ // to the end of the value — which would silently turn "replace the word I
63
+ // am typing" into "append to it".
64
+ const rememberCaret = () => {
65
+ const el = inputRef.current;
66
+ if (!el) return;
67
+ caret.current = {
68
+ start: el.selectionStart ?? el.value.length,
69
+ end: el.selectionEnd ?? el.value.length,
70
+ };
71
+ };
72
+
73
+ const pick = (entry: VariableEntry) => {
74
+ const { start, end } = caret.current;
75
+ const next = applyInsert(value, entry, mode, start, end);
76
+ onChange(next.value);
77
+ setOpen(false);
78
+ requestAnimationFrame(() => {
79
+ const el = inputRef.current;
80
+ if (!el) return;
81
+ el.focus();
82
+ el.setSelectionRange(next.caret, next.caret);
83
+ caret.current = { start: next.caret, end: next.caret };
84
+ });
85
+ };
86
+
87
+ return (
88
+ <div className="ac-varpick__input">
89
+ <InputText
90
+ ref={inputRef}
91
+ value={value}
92
+ placeholder={placeholder}
93
+ disabled={disabled}
94
+ className={`${className ?? ''}${invalid ? ' p-invalid' : ''}`}
95
+ onChange={(e) => onChange(e.target.value)}
96
+ onSelect={rememberCaret}
97
+ onFocus={rememberCaret}
98
+ />
99
+ <Button
100
+ type="button"
101
+ icon="pi pi-list"
102
+ aria-label={ariaLabel}
103
+ disabled={disabled}
104
+ className="p-button-outlined"
105
+ onClick={() => { setBuilt(catalog()); setOpen(true); }}
106
+ />
107
+ {open && (
108
+ <VariablePicker
109
+ visible
110
+ catalog={built}
111
+ title={pickerTitle}
112
+ hint={pickerHint}
113
+ initialQuery={
114
+ mode === 'caret'
115
+ ? queryFromCaret(value, caret.current.start, caret.current.end)
116
+ : ''
117
+ }
118
+ onCancel={() => setOpen(false)}
119
+ onPick={pick}
120
+ />
121
+ )}
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export default VariableInput;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * VariablePicker — a searchable, described list of the machine's variables.
3
+ *
4
+ * ## Why
5
+ *
6
+ * Every place the TIS method editor asks an author to name a machine value was
7
+ * a bare text box: a field's `Source` hinting `gm.<variable_name>`, a bound
8
+ * hinting `${gm.…}`. Nobody can type a variable out of a program they cannot
9
+ * see, and a wrong name fails SILENTLY in both directions — a `source` that
10
+ * names nothing never binds, a bound that resolves to nothing is simply
11
+ * unbounded.
12
+ *
13
+ * Typing is still allowed. The button is additive: it opens the list, shows what
14
+ * each variable MEANS, and writes the right spelling for the field it came from.
15
+ *
16
+ * ## Rules honoured here
17
+ *
18
+ * - **No tooltips, no `title`.** Half the installed base is a touchscreen with
19
+ * no hover, so the description is on the row, not behind it.
20
+ * - **Pick-then-Insert.** A stray tap in a list must not rewrite a method.
21
+ * - **A disabled entry is shown with its reason**, never omitted — hiding it
22
+ * answers "where is my variable?" with the same silence as a text box.
23
+ *
24
+ * The dialog's own state is deliberately trivial (a query and a selection) and
25
+ * it is unmounted between openings, so there is no draft here to reseed — see
26
+ * `useEditDraft` for the fields that DO have one.
27
+ */
28
+
29
+ import * as React from 'react';
30
+ import { Dialog } from 'primereact/dialog';
31
+ import { Button } from 'primereact/button';
32
+ import { InputText } from 'primereact/inputtext';
33
+ import {
34
+ firstChoosable,
35
+ searchCatalog,
36
+ type Catalog,
37
+ type VariableEntry,
38
+ } from '@adcops/autocore-varpick';
39
+ import './varpick.css';
40
+
41
+ export interface VariablePickerProps {
42
+ visible: boolean;
43
+ catalog: Catalog;
44
+ /** Dialog heading. */
45
+ title?: string;
46
+ /** One line saying what picking will do to the field. */
47
+ hint?: React.ReactNode;
48
+ /** Seeds the search box — usually the word the caret was sitting in. */
49
+ initialQuery?: string;
50
+ onCancel: () => void;
51
+ onPick: (entry: VariableEntry) => void;
52
+ }
53
+
54
+ export const VariablePicker: React.FC<VariablePickerProps> = ({
55
+ visible, catalog, title, hint, initialQuery, onCancel, onPick,
56
+ }) => {
57
+ const [query, setQuery] = React.useState(initialQuery ?? '');
58
+ const [selected, setSelected] = React.useState<string | null>(null);
59
+
60
+ // Reset on each opening. The dialog is cheap and stateless by design; the
61
+ // alternative — remembering the last search — means opening the picker on a
62
+ // different field shows a list filtered for the previous one.
63
+ React.useEffect(() => {
64
+ if (visible) setQuery(initialQuery ?? '');
65
+ }, [visible, initialQuery]);
66
+
67
+ const filtered = React.useMemo(() => searchCatalog(catalog, query), [catalog, query]);
68
+
69
+ // The highlight follows the search, so Enter always takes the top hit
70
+ // rather than a stale selection that has scrolled out of view.
71
+ React.useEffect(() => {
72
+ setSelected(firstChoosable(filtered)?.id ?? null);
73
+ }, [filtered]);
74
+
75
+ const entryById = (id: string | null): VariableEntry | undefined =>
76
+ id === null ? undefined : filtered.flatMap(g => g.entries).find(e => e.id === id);
77
+
78
+ const confirm = (entry?: VariableEntry) => {
79
+ const chosen = entry ?? entryById(selected);
80
+ if (!chosen || chosen.disabled) return;
81
+ onPick(chosen);
82
+ };
83
+
84
+ const footer = (
85
+ <div className="ac-varpick__foot">
86
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} />
87
+ <Button
88
+ label="Insert"
89
+ disabled={entryById(selected) === undefined}
90
+ onClick={() => confirm()}
91
+ />
92
+ </div>
93
+ );
94
+
95
+ return (
96
+ <Dialog
97
+ header={title ?? 'Insert a variable'}
98
+ visible={visible}
99
+ onHide={onCancel}
100
+ footer={footer}
101
+ style={{ width: '44rem', maxWidth: '95vw' }}
102
+ >
103
+ {hint && <div className="ac-varpick__hint">{hint}</div>}
104
+
105
+ <InputText
106
+ className="ac-varpick__search"
107
+ value={query}
108
+ autoFocus
109
+ placeholder="name, or what it means — try “load” or “EL3356”"
110
+ onChange={(e) => setQuery(e.target.value)}
111
+ onKeyDown={(e) => {
112
+ if (e.key === 'Enter') { e.preventDefault(); confirm(); }
113
+ }}
114
+ />
115
+
116
+ {filtered.length === 0 ? (
117
+ <div className="ac-varpick__empty">
118
+ {catalog.length === 0
119
+ ? 'Nothing to pick from. The machine published no variables — the server may be an older build without gm.get_variables.'
120
+ : `No variable matches “${query}”.`}
121
+ </div>
122
+ ) : (
123
+ <div className="ac-varpick__list">
124
+ {filtered.map(group => (
125
+ <div className="ac-varpick__group" key={group.id}>
126
+ <div className="ac-varpick__grouphead">
127
+ <span className="ac-varpick__grouptitle">{group.title}</span>
128
+ {group.hint && <span className="ac-varpick__grouphint">{group.hint}</span>}
129
+ </div>
130
+ {group.entries.map(entry => (
131
+ <button
132
+ type="button"
133
+ key={entry.id}
134
+ className={
135
+ 'ac-varpick__row'
136
+ + (entry.id === selected ? ' ac-varpick__row--on' : '')
137
+ + (entry.disabled ? ' ac-varpick__row--off' : '')
138
+ }
139
+ aria-pressed={entry.id === selected}
140
+ disabled={!!entry.disabled}
141
+ onClick={() => setSelected(entry.id)}
142
+ onDoubleClick={() => confirm(entry)}
143
+ >
144
+ <span className="ac-varpick__line">
145
+ <code className="ac-varpick__name">{entry.name}</code>
146
+ {entry.label && <span className="ac-varpick__label">{entry.label}</span>}
147
+ {entry.unit && <span className="ac-varpick__chip">{entry.unit}</span>}
148
+ {entry.type && <span className="ac-varpick__chip">{entry.type}</span>}
149
+ {entry.live !== undefined && (
150
+ <span className="ac-varpick__live">now {entry.live}</span>
151
+ )}
152
+ </span>
153
+ {entry.description && (
154
+ <span className="ac-varpick__desc">{entry.description}</span>
155
+ )}
156
+ {entry.disabled && <span className="ac-varpick__why">{entry.disabled}</span>}
157
+ {entry.note && <span className="ac-varpick__note">{entry.note}</span>}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ ))}
162
+ </div>
163
+ )}
164
+ </Dialog>
165
+ );
166
+ };
167
+
168
+ export default VariablePicker;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The variable picker: a field with a button that opens a searchable, described
3
+ * list of the machine's variables.
4
+ *
5
+ * The model (catalog shape, search ranking, caret-aware insertion, the `${fqdn}`
6
+ * token grammar) lives in `@adcops/autocore-varpick`, which has no UI in it —
7
+ * the sequence editor renders the same model with a different widget layer, and
8
+ * neither package may import the other's.
9
+ */
10
+
11
+ export { VariablePicker } from './VariablePicker';
12
+ export type { VariablePickerProps } from './VariablePicker';
13
+ export { VariableInput } from './VariableInput';
14
+ export type { VariableInputProps } from './VariableInput';
15
+ export {
16
+ boundCatalog,
17
+ chartFieldCatalog,
18
+ configSourceCatalog,
19
+ cycleSourceCatalog,
20
+ rawSourceCatalog,
21
+ } from './tisCatalogs';
22
+ export type { TisCatalogContext } from './tisCatalogs';