@adcops/autocore-react 3.5.7 → 3.5.9

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 (37) 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 +2 -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/editor/ChartViewDialog.d.ts.map +1 -1
  16. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  17. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  18. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  19. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  20. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  21. package/dist/components/tis-editor/types.d.ts +6 -0
  22. package/dist/components/tis-editor/types.d.ts.map +1 -1
  23. package/package.json +3 -1
  24. package/src/components/forms/useEditDraft.ts +0 -0
  25. package/src/components/index.ts +3 -0
  26. package/src/components/tis/TestDataView.tsx +16 -10
  27. package/src/components/tis/TestRawDataView.tsx +6 -7
  28. package/src/components/tis/chartViews.ts +104 -0
  29. package/src/components/tis-editor/editor/ChartViewDialog.tsx +24 -10
  30. package/src/components/tis-editor/editor/TestFieldDialog.tsx +25 -12
  31. package/src/components/tis-editor/editor/ViewsEditor.tsx +31 -5
  32. package/src/components/tis-editor/types.ts +6 -0
  33. package/tools/tests/dist/tis-editor.entry.css +90 -0
  34. package/tools/tests/dist/tis-editor.entry.js +41431 -0
  35. package/tools/tests/tis-editor.entry.tsx +46 -0
  36. package/tools/tests/tis-editor.test.mjs +133 -0
  37. package/tools/tests/vite.config.mjs +30 -0
@@ -24,6 +24,7 @@ import { Line } from 'react-chartjs-2';
24
24
  import type { ChartView, TestMethod } from './TestDataView';
25
25
  import { useTis } from './TisProvider';
26
26
  import { useRawCycleData } from './useRawCycleData';
27
+ import { orderedViews } from './chartViews';
27
28
 
28
29
  ChartJS.register(
29
30
  CategoryScale, LinearScale, PointElement, LineElement,
@@ -55,13 +56,11 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
55
56
  const chartRef = useRef<any>(null);
56
57
 
57
58
  // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
58
- const traceViews = useMemo(() => {
59
- const out: { name: string; view: ChartView }[] = [];
60
- for (const [name, v] of Object.entries(schema?.views ?? {})) {
61
- if ((v as ChartView).type === 'raw_trace') out.push({ name, view: v as ChartView });
62
- }
63
- return out;
64
- }, [schema]);
59
+ // In the author's order, same as the unified panel's picker.
60
+ const traceViews = useMemo(
61
+ () => orderedViews(schema?.views as Record<string, ChartView> | undefined, 'raw_trace'),
62
+ [schema],
63
+ );
65
64
 
66
65
  const [selectedView, setSelectedView] = useState<string | null>(
67
66
  traceViews.length > 0 ? traceViews[0].name : null,
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Chart-view ordering.
3
+ *
4
+ * `views` is a JSON object and the server holds it in a `BTreeMap`, so what
5
+ * reaches the HMI is sorted by view id — never by the order anyone chose. That
6
+ * order is a real setting: the Data tab opens on the FIRST view, so whichever
7
+ * id happens to sort first becomes the default chart. Renaming views to spell
8
+ * an order is not an option (the id is the wire key, and codegen emits it).
9
+ *
10
+ * So the order is carried explicitly, per view, as `order`: an integer,
11
+ * ascending, renumbered densely by the editor's Move up / Move down.
12
+ *
13
+ * `order` is deliberately NOT modelled by the Rust `ChartView`. It rides in
14
+ * that struct's flattened `extra` map, the way `x_decimals`, `smooth` and
15
+ * `regions` already do, so a `tis.put_method` → `tis.save_config` round-trip
16
+ * preserves it with no server-side change to deploy.
17
+ *
18
+ * A view with no `order` sorts after every ordered one, by id — so a method
19
+ * that has never been through the editor keeps exactly the order it shows
20
+ * today, and a newly added view lands last instead of jumping to the front.
21
+ */
22
+
23
+ /** Minimal shape this module needs; both the editor's and the runtime's
24
+ * `ChartView` satisfy it. */
25
+ export interface OrderableView {
26
+ order?: number;
27
+ type?: string;
28
+ }
29
+
30
+ /** A view together with its id — the id is the wire key, not a field. */
31
+ export interface NamedView<V> {
32
+ name: string;
33
+ view: V;
34
+ }
35
+
36
+ /** Sort key for a view that declares no `order`: after all ordered views. */
37
+ const UNORDERED = Number.MAX_SAFE_INTEGER;
38
+
39
+ const orderOf = (v: OrderableView | undefined): number =>
40
+ typeof v?.order === 'number' && Number.isFinite(v.order) ? v.order : UNORDERED;
41
+
42
+ /**
43
+ * The method's views in display order: by `order` ascending, then by id.
44
+ *
45
+ * @param views The method's `views` map, as received from the server.
46
+ * @param ofType Optional `type` filter (e.g. `'raw_trace'`).
47
+ */
48
+ export function orderedViews<V extends OrderableView>(
49
+ views?: Record<string, V> | null,
50
+ ofType?: string,
51
+ ): NamedView<V>[] {
52
+ const out: NamedView<V>[] = [];
53
+ for (const [name, view] of Object.entries(views ?? {})) {
54
+ if (ofType && view?.type !== ofType) continue;
55
+ out.push({ name, view });
56
+ }
57
+ out.sort((a, b) => {
58
+ const d = orderOf(a.view) - orderOf(b.view);
59
+ return d !== 0 ? d : a.name.localeCompare(b.name);
60
+ });
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Stamp `order` 0..n-1 onto `views` following `orderedIds`.
66
+ *
67
+ * Every view is renumbered, not just the moved one: dense integers mean the
68
+ * next move is a swap of two adjacent numbers, and a method that was
69
+ * previously unordered comes out fully ordered after one Move rather than
70
+ * half-and-half.
71
+ *
72
+ * Ids in `views` but missing from `orderedIds` keep their existing `order`
73
+ * and land after the renumbered ones.
74
+ */
75
+ export function renumberViews<V extends OrderableView>(
76
+ views: Record<string, V>,
77
+ orderedIds: string[],
78
+ ): Record<string, V> {
79
+ const out: Record<string, V> = {};
80
+ orderedIds.forEach((id, i) => {
81
+ const v = views[id];
82
+ if (v) out[id] = { ...v, order: i };
83
+ });
84
+ for (const [id, v] of Object.entries(views)) {
85
+ if (!(id in out)) out[id] = v;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * Move the view at `index` by `dir` (-1 up, +1 down) and renumber.
92
+ * Out-of-range moves return the map unchanged.
93
+ */
94
+ export function moveView<V extends OrderableView>(
95
+ views: Record<string, V>,
96
+ index: number,
97
+ dir: -1 | 1,
98
+ ): Record<string, V> {
99
+ const ids = orderedViews(views).map(v => v.name);
100
+ const j = index + dir;
101
+ if (index < 0 || index >= ids.length || j < 0 || j >= ids.length) return views;
102
+ [ids[index], ids[j]] = [ids[j], ids[index]];
103
+ return renumberViews(views, ids);
104
+ }
@@ -5,6 +5,7 @@ import { InputText } from 'primereact/inputtext';
5
5
  import { Dropdown } from 'primereact/dropdown';
6
6
  import { InputNumber } from 'primereact/inputnumber';
7
7
  import { FormRow } from '../../forms/FormRow';
8
+ import { useEditDraft } from '../../forms/useEditDraft';
8
9
  import { DEFAULT_X_DECIMALS } from '../../tis/TestDataView';
9
10
  import type { ChartAxis, ChartSeries, ChartView } from '../types';
10
11
 
@@ -43,17 +44,30 @@ const blank = (): ChartView => ({
43
44
  export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
44
45
  visible, initial, onCancel, onSave, knownKeys, siblingIds,
45
46
  }) => {
46
- const [viewId, setViewId] = useState<string>('');
47
- const [draft, setDraft] = useState<ChartView>(blank());
48
- const [error, setError] = useState<string | null>(null);
47
+ // One draft covering both edited values (the id is editable too), seeded
48
+ // once per open. Keyed on the view id rather than on `initial` — the
49
+ // parent rebuilds that object on every render of its own, and this editor
50
+ // re-renders with the live tag traffic underneath it, so an identity-keyed
51
+ // reseed threw away every keystroke as it was typed.
52
+ const { draft: form, setDraft: setForm, token } = useEditDraft(
53
+ visible,
54
+ initial?.viewId ?? '',
55
+ () => ({
56
+ viewId: initial?.viewId ?? '',
57
+ view: initial
58
+ ? (JSON.parse(JSON.stringify(initial.view)) as ChartView)
59
+ : blank(),
60
+ }),
61
+ );
62
+ const viewId = form.viewId;
63
+ const draft = form.view;
64
+ const setViewId = (id: string) => setForm(f => ({ ...f, viewId: id }));
65
+ const setDraft = (view: ChartView) => setForm(f => ({ ...f, view }));
49
66
 
50
- useEffect(() => {
51
- if (visible) {
52
- setViewId(initial?.viewId ?? '');
53
- setDraft(initial ? JSON.parse(JSON.stringify(initial.view)) : blank());
54
- setError(null);
55
- }
56
- }, [visible, initial]);
67
+ const [error, setError] = useState<string | null>(null);
68
+ // Clears on open and on a switch to another view — `token` changes on
69
+ // exactly those transitions and on no other render.
70
+ useEffect(() => { setError(null); }, [token]);
57
71
 
58
72
  const validate = (): string | null => {
59
73
  if (!viewId.trim()) return 'View ID is required.';
@@ -7,6 +7,7 @@ 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.
@@ -82,11 +83,31 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
82
83
  isResultsField = false, isCycleConfigField = false,
83
84
  cycleFieldNames = [], resultsFieldNames = [],
84
85
  }) => {
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>('');
86
+ // The field under edit plus its free-text Default, as one draft seeded
87
+ // once per open. `default` is edited as free text so a numeric literal, a
88
+ // string, or an `${fqdn}` token can all be entered in one field; it's
89
+ // coerced on save.
90
+ //
91
+ // Keyed on the field NAME, not on the `initial` object: this editor
92
+ // re-renders with the live tag traffic underneath it, and an
93
+ // identity-keyed reseed discards the operator's typing (see
94
+ // useEditDraft).
95
+ const { draft: form, setDraft: setForm, token } = useEditDraft(
96
+ visible,
97
+ initial?.name ?? '',
98
+ () => ({
99
+ field: initial ? { ...initial } : { ...blank },
100
+ defaultText: initial?.default == null ? '' : String(initial.default),
101
+ }),
102
+ );
103
+ const draft = form.field;
104
+ const defaultText = form.defaultText;
105
+ const setDraft = (field: TestField) => setForm(f => ({ ...f, field }));
106
+ const setDefaultText = (defaultText: string) => setForm(f => ({ ...f, defaultText }));
107
+
89
108
  const [error, setError] = useState<string | null>(null);
109
+ // Clears on open and on a switch to another field, and on no other render.
110
+ useEffect(() => { setError(null); }, [token]);
90
111
 
91
112
  // The quantity names this machine actually declares, from the resolved
92
113
  // units table. Offered as a dropdown so an author picks a row that exists
@@ -99,14 +120,6 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
99
120
  [scales],
100
121
  );
101
122
 
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]);
109
-
110
123
  // Turn the free-text Default into its stored form: an `${fqdn}` token and
111
124
  // other non-numerics stay strings; numeric/bool literals are stored typed.
112
125
  const coerceDefault = (s: string): unknown => {
@@ -3,6 +3,7 @@ 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';
8
9
 
@@ -19,8 +20,11 @@ interface ViewRow {
19
20
 
20
21
  export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) => {
21
22
  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 ?? '',
23
+ // Display order, not id order the operator sets it with the arrows and
24
+ // the runtime reads the same `order` key. The first row is the view the
25
+ // Data tab opens on.
26
+ const rows: ViewRow[] = orderedViews(views).map(({ name, view }) => ({
27
+ id: name, title: view.title ?? '', type: view.type ?? '',
24
28
  }));
25
29
 
26
30
  const [dialogOpen, setDialogOpen] = useState(false);
@@ -57,6 +61,14 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
57
61
  setDialogOpen(false);
58
62
  };
59
63
 
64
+ // Move up / down renumbers `order` densely across every view, so the next
65
+ // move is a swap of adjacent integers whatever the method started as.
66
+ const handleMove = (idx: number, dir: -1 | 1) => {
67
+ const next = moveView(views, idx, dir);
68
+ if (next === views) return;
69
+ onChange({ ...method, views: next });
70
+ };
71
+
60
72
  const handleRemove = (id: string) => {
61
73
  if (!window.confirm(`Remove view "${id}"?`)) return;
62
74
  const next = { ...views };
@@ -64,8 +76,22 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
64
76
  onChange({ ...method, views: next });
65
77
  };
66
78
 
67
- const rowActions = (r: ViewRow) => (
79
+ const rowActions = (r: ViewRow, opts: { rowIndex: number }) => (
68
80
  <div style={{ display: 'flex', gap: '0.25rem' }}>
81
+ <Button
82
+ icon="pi pi-arrow-up"
83
+ className="p-button-text p-button-sm"
84
+ disabled={opts.rowIndex === 0}
85
+ onClick={() => handleMove(opts.rowIndex, -1)}
86
+ aria-label="Move up"
87
+ />
88
+ <Button
89
+ icon="pi pi-arrow-down"
90
+ className="p-button-text p-button-sm"
91
+ disabled={opts.rowIndex === rows.length - 1}
92
+ onClick={() => handleMove(opts.rowIndex, 1)}
93
+ aria-label="Move down"
94
+ />
69
95
  <Button
70
96
  icon="pi pi-pencil"
71
97
  className="p-button-text p-button-sm"
@@ -85,14 +111,14 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
85
111
  <>
86
112
  <FormSection
87
113
  title="Views"
88
- description="Named chart definitions rendered by <TestDataView> (cycle_scatter) and <TestRawDataView> (raw_trace)."
114
+ 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
115
  actions={<Button label="Add view" icon="pi pi-plus" size="small" onClick={openNew} />}
90
116
  >
91
117
  <DataTable value={rows} dataKey="id" emptyMessage="No views defined.">
92
118
  <Column field="id" header="View ID" />
93
119
  <Column field="title" header="Title" />
94
120
  <Column field="type" header="Type" style={{ width: '8rem' }} />
95
- <Column header="" body={rowActions} style={{ width: '6rem' }} />
121
+ <Column header="" body={rowActions} style={{ width: '10rem' }} />
96
122
  </DataTable>
97
123
  </FormSection>
98
124
  <ChartViewDialog
@@ -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,90 @@
1
+ .ac-formsection {
2
+ border: 1px solid var(--surface-d, #e2e8f0);
3
+ border-radius: 6px;
4
+ background: var(--surface-card, #fff);
5
+ margin-bottom: 1rem;
6
+ }
7
+
8
+ .ac-formsection__header {
9
+ display: flex;
10
+ align-items: flex-start;
11
+ justify-content: space-between;
12
+ padding: 0.5rem 1rem;
13
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
14
+ background: var(--surface-b, #f8fafc);
15
+ border-top-left-radius: 6px;
16
+ border-top-right-radius: 6px;
17
+ }
18
+
19
+ .ac-formsection__title {
20
+ margin: 0;
21
+ font-size: 0.95rem;
22
+ font-weight: 600;
23
+ }
24
+
25
+ .ac-formsection__desc {
26
+ color: var(--text-color-secondary, #64748b);
27
+ display: block;
28
+ margin-top: 0.15rem;
29
+ }
30
+
31
+ .ac-formsection__actions {
32
+ display: flex;
33
+ gap: 0.5rem;
34
+ }
35
+
36
+ .ac-formsection__body {
37
+ padding: 0.75rem 1rem;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 0.5rem;
41
+ }
42
+
43
+ .ac-formrow {
44
+ display: grid;
45
+ grid-template-columns: minmax(8rem, 14rem) 1fr;
46
+ gap: 0.5rem 1rem;
47
+ align-items: start;
48
+ }
49
+
50
+ .ac-formrow--error .ac-formrow__field input,
51
+ .ac-formrow--error .ac-formrow__field .p-inputtext {
52
+ border-color: #dc2626;
53
+ }
54
+
55
+ .ac-formrow__label {
56
+ font-weight: 500;
57
+ padding-top: 0.4rem;
58
+ display: flex;
59
+ flex-direction: column;
60
+ }
61
+
62
+ .ac-formrow__required {
63
+ color: #dc2626;
64
+ }
65
+
66
+ .ac-formrow__hint {
67
+ color: var(--text-color-secondary, #64748b);
68
+ font-weight: 400;
69
+ font-size: 0.75rem;
70
+ margin-top: 0.15rem;
71
+ }
72
+
73
+ .ac-formrow__field {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 0.25rem;
77
+ }
78
+
79
+ .ac-formrow__field > input,
80
+ .ac-formrow__field > .p-inputtext,
81
+ .ac-formrow__field > .p-dropdown,
82
+ .ac-formrow__field > .p-inputtextarea {
83
+ width: 100%;
84
+ }
85
+
86
+ .ac-formrow__error {
87
+ color: #dc2626;
88
+ font-size: 0.75rem;
89
+ }
90
+ /*$vite$:1*/