@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.
- package/dist/components/forms/useEditDraft.d.ts +49 -0
- package/dist/components/forms/useEditDraft.d.ts.map +1 -0
- package/dist/components/forms/useEditDraft.js +1 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +8 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
- package/dist/components/tis/TestRawDataView.js +1 -1
- package/dist/components/tis/chartViews.d.ts +57 -0
- package/dist/components/tis/chartViews.d.ts.map +1 -0
- package/dist/components/tis/chartViews.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +11 -0
- 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/AuthoringContext.d.ts +37 -0
- package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
- 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/RawDataEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -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 +6 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/varpick/VariableInput.d.ts +41 -0
- package/dist/components/varpick/VariableInput.d.ts.map +1 -0
- package/dist/components/varpick/VariableInput.js +1 -0
- package/dist/components/varpick/VariablePicker.d.ts +45 -0
- package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
- package/dist/components/varpick/VariablePicker.js +1 -0
- package/dist/components/varpick/index.d.ts +16 -0
- package/dist/components/varpick/index.d.ts.map +1 -0
- package/dist/components/varpick/index.js +1 -0
- package/dist/components/varpick/tisCatalogs.d.ts +85 -0
- package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
- package/dist/components/varpick/tisCatalogs.js +1 -0
- package/dist/components/varpick/varpick.css +167 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/core/AutoCoreTagTypes.d.ts +13 -0
- package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useGmVariables.d.ts +57 -0
- package/dist/hooks/useGmVariables.d.ts.map +1 -0
- package/dist/hooks/useGmVariables.js +1 -0
- package/dist/hooks/useSequenceProducers.d.ts +38 -0
- package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
- package/dist/hooks/useSequenceProducers.js +1 -0
- package/package.json +7 -2
- package/src/components/forms/useEditDraft.ts +0 -0
- package/src/components/index.ts +20 -0
- package/src/components/tis/TestDataView.tsx +16 -10
- package/src/components/tis/TestRawDataView.tsx +6 -7
- package/src/components/tis/chartViews.ts +104 -0
- package/src/components/tis-editor/TisConfigEditor.css +11 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
- package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
- package/src/components/tis-editor/editor/ChartViewDialog.tsx +48 -17
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
- package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +90 -23
- package/src/components/tis-editor/editor/ViewsEditor.tsx +56 -5
- package/src/components/tis-editor/types.ts +6 -0
- package/src/components/varpick/VariableInput.tsx +126 -0
- package/src/components/varpick/VariablePicker.tsx +168 -0
- package/src/components/varpick/index.ts +22 -0
- package/src/components/varpick/tisCatalogs.ts +235 -0
- package/src/components/varpick/varpick.css +167 -0
- package/src/core/AutoCoreTagContext.tsx +3 -1
- package/src/core/AutoCoreTagTypes.ts +14 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useGmVariables.ts +142 -0
- package/src/hooks/useSequenceProducers.ts +104 -0
- package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
- package/tools/tests/dist/autocore-react.css +257 -0
- package/tools/tests/dist/tis-editor.entry.js +9295 -0
- package/tools/tests/dist/varpick.entry.js +113 -0
- package/tools/tests/live-server.probe.mjs +172 -0
- package/tools/tests/tis-editor.entry.tsx +46 -0
- package/tools/tests/tis-editor.test.mjs +133 -0
- package/tools/tests/varpick.entry.tsx +81 -0
- package/tools/tests/varpick.test.mjs +208 -0
- 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
|
-
<
|
|
93
|
+
<VariableInput
|
|
93
94
|
placeholder="source (time / ni.<daq>.channels.<name> / derived)"
|
|
94
95
|
value={col.source}
|
|
95
|
-
onChange={(
|
|
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
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
266
|
-
|
|
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={(
|
|
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
|
-
<
|
|
342
|
+
<VariableInput
|
|
288
343
|
value={boundText(draft.min)}
|
|
289
|
-
onChange={(
|
|
290
|
-
|
|
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
|
-
<
|
|
357
|
+
<VariableInput
|
|
299
358
|
value={boundText(draft.max)}
|
|
300
|
-
onChange={(
|
|
301
|
-
|
|
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
|
-
<
|
|
369
|
+
<VariableInput
|
|
307
370
|
value={defaultText}
|
|
308
|
-
onChange={
|
|
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
|
-
|
|
23
|
-
|
|
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: '
|
|
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';
|