@adcops/autocore-react 3.5.9 → 3.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- 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/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 +5 -2
- package/src/components/index.ts +17 -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 +24 -7
- 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 +66 -12
- package/src/components/tis-editor/editor/ViewsEditor.tsx +25 -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 +8431 -40567
- package/tools/tests/dist/varpick.entry.js +113 -0
- package/tools/tests/live-server.probe.mjs +172 -0
- package/tools/tests/varpick.entry.tsx +81 -0
- package/tools/tests/varpick.test.mjs +208 -0
- package/tools/tests/vite.config.mjs +7 -2
- package/tools/tests/dist/tis-editor.entry.css +0 -90
|
@@ -8,6 +8,8 @@ import { FormRow } from '../../forms/FormRow';
|
|
|
8
8
|
import { useEditDraft } from '../../forms/useEditDraft';
|
|
9
9
|
import { DEFAULT_X_DECIMALS } from '../../tis/TestDataView';
|
|
10
10
|
import type { ChartAxis, ChartSeries, ChartView } from '../types';
|
|
11
|
+
import { VariableInput } from '../../varpick';
|
|
12
|
+
import type { Catalog } from '@adcops/autocore-varpick';
|
|
11
13
|
|
|
12
14
|
/** Upper bound on the editor's X-decimals spinner. Past ~6 the tick labels
|
|
13
15
|
* are wider than the gap between them, which is the problem this setting
|
|
@@ -31,6 +33,16 @@ export interface ChartViewDialogProps {
|
|
|
31
33
|
onSave: (viewId: string, view: ChartView) => void;
|
|
32
34
|
/** Known field/column names; offered as datalist suggestions. */
|
|
33
35
|
knownKeys: string[];
|
|
36
|
+
/**
|
|
37
|
+
* The same names, grouped and described, for the picker button.
|
|
38
|
+
*
|
|
39
|
+
* Takes the view TYPE because it decides which list is the likely answer:
|
|
40
|
+
* a `raw_trace` plots trace columns, a `cycle_scatter` plots cycle fields.
|
|
41
|
+
* A datalist alone was not enough — it only appears once you start typing,
|
|
42
|
+
* which is no help when you do not know what to type, and it does not open
|
|
43
|
+
* at all on a touchscreen.
|
|
44
|
+
*/
|
|
45
|
+
fieldCatalog?: (viewType: string) => Catalog;
|
|
34
46
|
/** Other view ids already in use — for uniqueness validation. */
|
|
35
47
|
siblingIds: string[];
|
|
36
48
|
}
|
|
@@ -42,7 +54,7 @@ const blank = (): ChartView => ({
|
|
|
42
54
|
});
|
|
43
55
|
|
|
44
56
|
export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
|
|
45
|
-
visible, initial, onCancel, onSave, knownKeys, siblingIds,
|
|
57
|
+
visible, initial, onCancel, onSave, knownKeys, siblingIds, fieldCatalog,
|
|
46
58
|
}) => {
|
|
47
59
|
// One draft covering both edited values (the id is editable too), seeded
|
|
48
60
|
// once per open. Keyed on the view id rather than on `initial` — the
|
|
@@ -124,10 +136,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
|
|
|
124
136
|
/>
|
|
125
137
|
</FormRow>
|
|
126
138
|
<FormRow label={isRawTrace ? 'X column' : 'X field'} required>
|
|
127
|
-
<
|
|
139
|
+
<VariableInput
|
|
128
140
|
value={(draft.x[axisKey] as string) ?? ''}
|
|
129
|
-
onChange={(
|
|
130
|
-
|
|
141
|
+
onChange={(v) => setAxis({ [axisKey]: v } as ChartAxis)}
|
|
142
|
+
catalog={() => fieldCatalog?.(draft.type) ?? []}
|
|
143
|
+
ariaLabel="Choose the X axis column"
|
|
144
|
+
pickerTitle="X axis"
|
|
145
|
+
pickerHint="A chart naming a column that does not exist draws an empty plot with no error, which reads as “the machine recorded nothing”."
|
|
131
146
|
/>
|
|
132
147
|
</FormRow>
|
|
133
148
|
<FormRow label="X label">
|
|
@@ -173,11 +188,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
|
|
|
173
188
|
alignItems: 'center',
|
|
174
189
|
}}
|
|
175
190
|
>
|
|
176
|
-
<
|
|
191
|
+
<VariableInput
|
|
177
192
|
placeholder={isRawTrace ? 'column' : 'field'}
|
|
178
193
|
value={(s[axisKey] as string) ?? ''}
|
|
179
|
-
onChange={(
|
|
180
|
-
|
|
194
|
+
onChange={(v) => setSeries(i, { [axisKey]: v } as ChartSeries)}
|
|
195
|
+
catalog={() => fieldCatalog?.(draft.type) ?? []}
|
|
196
|
+
ariaLabel={`Choose the column for series ${i + 1}`}
|
|
197
|
+
pickerTitle="Y series"
|
|
181
198
|
/>
|
|
182
199
|
<InputText
|
|
183
200
|
placeholder="label"
|
|
@@ -148,6 +148,7 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
|
|
|
148
148
|
siblingNames={fields.map(f => f.name)}
|
|
149
149
|
isResultsField={arrayKey === 'results_fields'}
|
|
150
150
|
isCycleConfigField={arrayKey === 'cycle_config_fields'}
|
|
151
|
+
isCycleField={arrayKey === 'cycle_fields'}
|
|
151
152
|
cycleFieldNames={(method.cycle_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
152
153
|
resultsFieldNames={(method.results_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
153
154
|
onCancel={() => setDialogOpen(false)}
|
|
@@ -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)"
|
|
@@ -13,6 +13,9 @@ import type { TestField, AggFn, AggFrom, AggScope, AggregateSpec } from '../type
|
|
|
13
13
|
// form so the editor and the runtime can never disagree on what a bound is.
|
|
14
14
|
import { boundText, parseBound, boundInvalid } from '../../tis/TestFieldRow';
|
|
15
15
|
import { AutoCoreTagContext } from '../../../core/AutoCoreTagContext';
|
|
16
|
+
import { VariableInput } from '../../varpick';
|
|
17
|
+
import { boundCatalog, configSourceCatalog, cycleSourceCatalog } from '../../varpick';
|
|
18
|
+
import { useAuthoringCatalogs } from './AuthoringContext';
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
const FIELD_TYPES: { label: string; value: string }[] = [
|
|
@@ -74,15 +77,25 @@ export interface TestFieldDialogProps {
|
|
|
74
77
|
/** Candidate target field names for an aggregate's `of`, by source. */
|
|
75
78
|
cycleFieldNames?: string[];
|
|
76
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;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
const blank: TestField = { name: '', type: 'f32' };
|
|
80
90
|
|
|
81
91
|
export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
82
92
|
visible, initial, onCancel, onSave, siblingNames,
|
|
83
|
-
isResultsField = false, isCycleConfigField = false,
|
|
93
|
+
isResultsField = false, isCycleConfigField = false, isCycleField = false,
|
|
84
94
|
cycleFieldNames = [], resultsFieldNames = [],
|
|
85
95
|
}) => {
|
|
96
|
+
// Supplied by the editor above; see AuthoringContext for why this is not
|
|
97
|
+
// four levels of props.
|
|
98
|
+
const { variables, producedNames } = useAuthoringCatalogs();
|
|
86
99
|
// The field under edit plus its free-text Default, as one draft seeded
|
|
87
100
|
// once per open. `default` is edited as free text so a numeric literal, a
|
|
88
101
|
// string, or an `${fqdn}` token can all be entered in one field; it's
|
|
@@ -120,6 +133,24 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
120
133
|
[scales],
|
|
121
134
|
);
|
|
122
135
|
|
|
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);
|
|
153
|
+
|
|
123
154
|
// Turn the free-text Default into its stored form: an `${fqdn}` token and
|
|
124
155
|
// other non-numerics stay strings; numeric/bool literals are stored typed.
|
|
125
156
|
const coerceDefault = (s: string): unknown => {
|
|
@@ -275,10 +306,21 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
275
306
|
/>
|
|
276
307
|
</FormRow>
|
|
277
308
|
)}
|
|
278
|
-
<FormRow
|
|
279
|
-
|
|
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
|
|
280
316
|
value={draft.source ?? ''}
|
|
281
|
-
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.'}
|
|
282
324
|
placeholder="gm.<variable_name>"
|
|
283
325
|
/>
|
|
284
326
|
</FormRow>
|
|
@@ -297,10 +339,14 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
297
339
|
label="Min"
|
|
298
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."
|
|
299
341
|
>
|
|
300
|
-
<
|
|
342
|
+
<VariableInput
|
|
301
343
|
value={boundText(draft.min)}
|
|
302
|
-
onChange={(
|
|
303
|
-
|
|
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."
|
|
304
350
|
placeholder="number or ${gm.…}"
|
|
305
351
|
/>
|
|
306
352
|
</FormRow>
|
|
@@ -308,17 +354,25 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
308
354
|
label="Max"
|
|
309
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}."
|
|
310
356
|
>
|
|
311
|
-
<
|
|
357
|
+
<VariableInput
|
|
312
358
|
value={boundText(draft.max)}
|
|
313
|
-
onChange={(
|
|
314
|
-
|
|
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."
|
|
315
365
|
placeholder="number or ${gm.…}"
|
|
316
366
|
/>
|
|
317
367
|
</FormRow>
|
|
318
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.">
|
|
319
|
-
<
|
|
369
|
+
<VariableInput
|
|
320
370
|
value={defaultText}
|
|
321
|
-
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."
|
|
322
376
|
placeholder="e.g. 5 or ${gm.safe_speed}"
|
|
323
377
|
/>
|
|
324
378
|
</FormRow>
|
|
@@ -6,6 +6,8 @@ import { FormSection } from '../../forms/FormSection';
|
|
|
6
6
|
import { moveView, orderedViews } from '../../tis/chartViews';
|
|
7
7
|
import { ChartViewDialog } from './ChartViewDialog';
|
|
8
8
|
import type { ChartView, TestField, TestMethod } from '../types';
|
|
9
|
+
import { chartFieldCatalog } from '../../varpick';
|
|
10
|
+
import type { Catalog } from '@adcops/autocore-varpick';
|
|
9
11
|
|
|
10
12
|
export interface ViewsEditorProps {
|
|
11
13
|
method: TestMethod;
|
|
@@ -47,6 +49,28 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
|
|
|
47
49
|
'timestamp',
|
|
48
50
|
];
|
|
49
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
|
+
|
|
50
74
|
const openNew = () => { setEditingId(null); setDialogOpen(true); };
|
|
51
75
|
const openEdit = (id: string) => { setEditingId(id); setDialogOpen(true); };
|
|
52
76
|
|
|
@@ -125,6 +149,7 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
|
|
|
125
149
|
visible={dialogOpen}
|
|
126
150
|
initial={editingId ? { viewId: editingId, view: views[editingId] } : null}
|
|
127
151
|
knownKeys={knownKeys}
|
|
152
|
+
fieldCatalog={fieldCatalog}
|
|
128
153
|
siblingIds={Object.keys(views)}
|
|
129
154
|
onCancel={() => setDialogOpen(false)}
|
|
130
155
|
onSave={handleSave}
|
|
@@ -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';
|