@adcops/autocore-react 3.5.15 → 3.6.5
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/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/components/tis-editor/DeleteMethodDialog.d.ts +43 -0
- package/dist/components/tis-editor/DeleteMethodDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/DeleteMethodDialog.js +1 -0
- package/dist/components/tis-editor/MethodTransfer.d.ts +69 -0
- package/dist/components/tis-editor/MethodTransfer.d.ts.map +1 -0
- package/dist/components/tis-editor/MethodTransfer.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +31 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts +9 -1
- 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/IdentitySection.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/IdentitySection.js +1 -1
- package/dist/components/tis-editor/types.d.ts +11 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/varpick/VariablePicker.d.ts.map +1 -1
- package/dist/components/varpick/VariablePicker.js +1 -1
- package/dist/components/varpick/varpick.css +84 -30
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useMethodSequence.d.ts +60 -0
- package/dist/hooks/useMethodSequence.d.ts.map +1 -0
- package/dist/hooks/useMethodSequence.js +1 -0
- package/package.json +1 -1
- package/src/components/tis/TestDataView.tsx +39 -1
- package/src/components/tis/TisProvider.tsx +54 -9
- package/src/components/tis-editor/DeleteMethodDialog.tsx +162 -0
- package/src/components/tis-editor/MethodTransfer.ts +138 -0
- package/src/components/tis-editor/TisConfigEditor.css +31 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +305 -7
- package/src/components/tis-editor/editor/IdentitySection.tsx +33 -1
- package/src/components/tis-editor/types.ts +11 -0
- package/src/components/varpick/VariablePicker.tsx +4 -0
- package/src/components/varpick/varpick.css +84 -30
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useMethodSequence.ts +128 -0
- package/tools/tests/dist/{TestFieldDialog-BvHAr2GQ.js → TestFieldDialog-Dph2oPfe.js} +1 -0
- package/tools/tests/dist/autocore-react.css +84 -30
- package/tools/tests/dist/tis-editor.entry.js +1 -1
- package/tools/tests/dist/varpick.entry.js +1 -1
- package/tools/tests/varpick.test.mjs +5 -0
|
@@ -1,12 +1,66 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Variable picker.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* ## The token bridge
|
|
5
|
+
*
|
|
6
|
+
* Every colour resolves `--ac-*` first, then the PrimeReact theme variable,
|
|
7
|
+
* then a literal. That ordering is the whole point:
|
|
8
|
+
*
|
|
9
|
+
* - **`--ac-*`** is autocore's own design-token layer, which 4.0 supplies.
|
|
10
|
+
* When it exists, this file needs no edit to follow it.
|
|
11
|
+
* - **PrimeReact's `--surface-*` / `--text-color`** is what 3.5 actually
|
|
12
|
+
* defines today, so the picker matches whatever theme the machine runs,
|
|
13
|
+
* light or dark.
|
|
14
|
+
* - **The literal** keeps an un-themed host legible — the playground, the
|
|
15
|
+
* jsdom test harness — rather than rendering black on black.
|
|
16
|
+
*
|
|
17
|
+
* The token NAMES and their fallbacks are copied deliberately from
|
|
18
|
+
* `@adcops/autocore-seq-react`'s `styles.css`. The sequence editor's picker and
|
|
19
|
+
* this one sit one tab apart doing the identical thing; if they read different
|
|
20
|
+
* token names, 4.0 would have to satisfy two sets and the two panels could
|
|
21
|
+
* drift to different colours while both looked "correct".
|
|
22
|
+
*
|
|
23
|
+
* ## Why the tokens are declared twice
|
|
24
|
+
*
|
|
25
|
+
* The field lives inside the TIS editor's form, but PrimeReact renders the
|
|
26
|
+
* Dialog through a PORTAL to `<body>` — so the two halves are not in one
|
|
27
|
+
* subtree and a single scoping root cannot cover both. Hence the declaration on
|
|
28
|
+
* `.ac-varpick` (the dialog, which passes that className) and on
|
|
29
|
+
* `.ac-varpick__input` (the field). Declaring them on `:root` instead would
|
|
30
|
+
* push a dozen names into every consuming application's global scope for the
|
|
31
|
+
* benefit of one component.
|
|
8
32
|
*/
|
|
9
33
|
|
|
34
|
+
.ac-varpick,
|
|
35
|
+
.ac-varpick__input {
|
|
36
|
+
/* -- surfaces ---------------------------------------------------- */
|
|
37
|
+
--vp-sunk: var(--ac-surface-sunk, var(--surface-50, #f8f9fa));
|
|
38
|
+
--vp-overlay: var(--ac-surface-overlay, var(--surface-overlay,#ffffff));
|
|
39
|
+
--vp-hover: var(--ac-hover, var(--surface-hover, rgba(127, 127, 127, 0.08)));
|
|
40
|
+
|
|
41
|
+
/* -- ink --------------------------------------------------------- */
|
|
42
|
+
--vp-ink: var(--ac-on-surface, var(--text-color, #111827));
|
|
43
|
+
--vp-ink-muted: var(--ac-on-surface-muted, var(--text-color-secondary, #6b7280));
|
|
44
|
+
--vp-ink-subtle: var(--ac-on-surface-subtle, var(--text-color-secondary, #8b95a1));
|
|
45
|
+
|
|
46
|
+
/* -- lines ------------------------------------------------------- */
|
|
47
|
+
--vp-border: var(--ac-border, var(--surface-border, rgba(127, 127, 127, 0.25)));
|
|
48
|
+
--vp-divider: var(--ac-divider, var(--surface-border, rgba(127, 127, 127, 0.18)));
|
|
49
|
+
|
|
50
|
+
/* -- accents ----------------------------------------------------- */
|
|
51
|
+
--vp-primary: var(--ac-primary, var(--primary-color, #2563eb));
|
|
52
|
+
--vp-primary-subtle: var(--ac-primary-subtle, var(--highlight-bg, rgba(37, 99, 235, 0.14)));
|
|
53
|
+
--vp-success: var(--ac-success, var(--green-500, #15803d));
|
|
54
|
+
--vp-warning: var(--ac-warning, var(--orange-500, #b45309));
|
|
55
|
+
--vp-info: var(--ac-info, var(--blue-500, #0369a1));
|
|
56
|
+
--vp-focus-ring: var(--ac-focus-ring, rgba(37, 99, 235, 0.45));
|
|
57
|
+
|
|
58
|
+
/* -- type -------------------------------------------------------- */
|
|
59
|
+
--vp-mono: var(--ac-font-mono, ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace);
|
|
60
|
+
--vp-font-sm: var(--ac-text-sm, 0.8125rem);
|
|
61
|
+
--vp-font-xs: var(--ac-text-xs, 0.75rem);
|
|
62
|
+
}
|
|
63
|
+
|
|
10
64
|
.ac-varpick__input {
|
|
11
65
|
display: flex;
|
|
12
66
|
align-items: stretch;
|
|
@@ -31,8 +85,8 @@
|
|
|
31
85
|
}
|
|
32
86
|
|
|
33
87
|
.ac-varpick__hint {
|
|
34
|
-
font-size:
|
|
35
|
-
color: var(--
|
|
88
|
+
font-size: var(--vp-font-sm);
|
|
89
|
+
color: var(--vp-ink-muted);
|
|
36
90
|
margin-bottom: 0.75rem;
|
|
37
91
|
line-height: 1.4;
|
|
38
92
|
}
|
|
@@ -41,7 +95,7 @@
|
|
|
41
95
|
|
|
42
96
|
.ac-varpick__empty {
|
|
43
97
|
padding: 1.5rem 0.25rem;
|
|
44
|
-
color: var(--
|
|
98
|
+
color: var(--vp-ink-muted);
|
|
45
99
|
font-size: 0.875rem;
|
|
46
100
|
}
|
|
47
101
|
|
|
@@ -64,24 +118,24 @@
|
|
|
64
118
|
gap: 0.5rem;
|
|
65
119
|
padding-bottom: 0.25rem;
|
|
66
120
|
margin-bottom: 0.25rem;
|
|
67
|
-
border-bottom: 1px solid var(--
|
|
121
|
+
border-bottom: 1px solid var(--vp-divider);
|
|
68
122
|
position: sticky;
|
|
69
123
|
top: 0;
|
|
70
|
-
background: var(--
|
|
124
|
+
background: var(--vp-overlay);
|
|
71
125
|
z-index: 1;
|
|
72
126
|
}
|
|
73
127
|
|
|
74
128
|
.ac-varpick__grouptitle {
|
|
75
|
-
font-size:
|
|
129
|
+
font-size: var(--vp-font-xs);
|
|
76
130
|
font-weight: 700;
|
|
77
131
|
letter-spacing: 0.04em;
|
|
78
132
|
text-transform: uppercase;
|
|
79
|
-
color: var(--
|
|
133
|
+
color: var(--vp-ink-muted);
|
|
80
134
|
}
|
|
81
135
|
|
|
82
136
|
.ac-varpick__grouphint {
|
|
83
|
-
font-size:
|
|
84
|
-
color: var(--
|
|
137
|
+
font-size: var(--vp-font-xs);
|
|
138
|
+
color: var(--vp-ink-subtle);
|
|
85
139
|
}
|
|
86
140
|
|
|
87
141
|
/*
|
|
@@ -102,21 +156,21 @@
|
|
|
102
156
|
border: 1px solid transparent;
|
|
103
157
|
border-radius: 3px;
|
|
104
158
|
background: transparent;
|
|
105
|
-
color: var(--
|
|
159
|
+
color: var(--vp-ink);
|
|
106
160
|
font: inherit;
|
|
107
161
|
cursor: pointer;
|
|
108
162
|
}
|
|
109
163
|
|
|
110
|
-
.ac-varpick__row:hover { background: var(--
|
|
164
|
+
.ac-varpick__row:hover { background: var(--vp-hover); }
|
|
111
165
|
|
|
112
166
|
.ac-varpick__row:focus-visible {
|
|
113
|
-
outline: 2px solid var(--
|
|
167
|
+
outline: 2px solid var(--vp-focus-ring);
|
|
114
168
|
outline-offset: -2px;
|
|
115
169
|
}
|
|
116
170
|
|
|
117
171
|
.ac-varpick__row--on {
|
|
118
|
-
background: var(--
|
|
119
|
-
border-color: var(--primary
|
|
172
|
+
background: var(--vp-primary-subtle);
|
|
173
|
+
border-color: var(--vp-primary);
|
|
120
174
|
}
|
|
121
175
|
|
|
122
176
|
.ac-varpick__row--off { opacity: 0.55; cursor: default; }
|
|
@@ -130,19 +184,19 @@
|
|
|
130
184
|
}
|
|
131
185
|
|
|
132
186
|
.ac-varpick__name {
|
|
133
|
-
font-family: var(--
|
|
187
|
+
font-family: var(--vp-mono);
|
|
134
188
|
font-weight: 600;
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
.ac-varpick__label { font-size: 0.875rem; }
|
|
138
192
|
|
|
139
193
|
.ac-varpick__chip {
|
|
140
|
-
font-size:
|
|
194
|
+
font-size: var(--vp-font-xs);
|
|
141
195
|
padding: 0 6px;
|
|
142
196
|
border-radius: 999px;
|
|
143
|
-
background: var(--
|
|
144
|
-
border: 1px solid var(--
|
|
145
|
-
color: var(--
|
|
197
|
+
background: var(--vp-sunk);
|
|
198
|
+
border: 1px solid var(--vp-border);
|
|
199
|
+
color: var(--vp-ink-muted);
|
|
146
200
|
/* A unit symbol's case is meaningful: "mm/s", "N", "MPa". */
|
|
147
201
|
text-transform: none;
|
|
148
202
|
}
|
|
@@ -150,18 +204,18 @@
|
|
|
150
204
|
/* The current reading. Nothing turns an unfamiliar name into an obvious one
|
|
151
205
|
* faster than seeing what it says right now. */
|
|
152
206
|
.ac-varpick__live {
|
|
153
|
-
font-family: var(--
|
|
154
|
-
font-size:
|
|
155
|
-
color: var(--
|
|
207
|
+
font-family: var(--vp-mono);
|
|
208
|
+
font-size: var(--vp-font-xs);
|
|
209
|
+
color: var(--vp-success);
|
|
156
210
|
}
|
|
157
211
|
|
|
158
212
|
.ac-varpick__desc {
|
|
159
|
-
font-size:
|
|
160
|
-
color: var(--
|
|
213
|
+
font-size: var(--vp-font-sm);
|
|
214
|
+
color: var(--vp-ink-muted);
|
|
161
215
|
line-height: 1.35;
|
|
162
216
|
}
|
|
163
217
|
|
|
164
|
-
.ac-varpick__why { font-size:
|
|
165
|
-
.ac-varpick__note { font-size:
|
|
218
|
+
.ac-varpick__why { font-size: var(--vp-font-sm); color: var(--vp-warning); }
|
|
219
|
+
.ac-varpick__note { font-size: var(--vp-font-sm); color: var(--vp-info); }
|
|
166
220
|
|
|
167
221
|
.ac-varpick__foot { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export { useGmVariables } from './useGmVariables';
|
|
|
5
5
|
export type { UseGmVariablesResult, UseGmVariablesOptions, GmIpcInvoker } from './useGmVariables';
|
|
6
6
|
export { useSequenceProducers, collectStoredNames } from './useSequenceProducers';
|
|
7
7
|
export type { UseSequenceProducersResult, UseSequenceProducersOptions } from './useSequenceProducers';
|
|
8
|
+
export { useMethodSequence, sequenceTopic, emptySequence, isSequenceShaped, } from './useMethodSequence';
|
|
9
|
+
export type { MethodSequenceIo, UseMethodSequenceOptions } from './useMethodSequence';
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,cAAc,mBAAmB,CAAC;AAKlC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIlG,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClF,YAAY,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,cAAc,mBAAmB,CAAC;AAKlC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIlG,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClF,YAAY,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACtG,OAAO,EACH,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,GACpE,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";export*from"./useServeletData";export{useGmVariables}from"./useGmVariables";export{useSequenceProducers,collectStoredNames}from"./useSequenceProducers";
|
|
1
|
+
export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";export*from"./useServeletData";export{useGmVariables}from"./useGmVariables";export{useSequenceProducers,collectStoredNames}from"./useSequenceProducers";export{useMethodSequence,sequenceTopic,emptySequence,isSequenceShaped}from"./useMethodSequence";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMethodSequence — read, write and delete the `.seq.json` half of a test
|
|
3
|
+
* method.
|
|
4
|
+
*
|
|
5
|
+
* ## One method, two files
|
|
6
|
+
*
|
|
7
|
+
* A sequence-backed test method is its `test_methods.json` schema entry PLUS
|
|
8
|
+
* `methods/<method_id>.seq.json`, the procedure the sequence engine runs. The
|
|
9
|
+
* filename is DERIVED from the method id and never stored, so the two halves
|
|
10
|
+
* cannot point at different things — see `autocore_tis::config::sequence_file`
|
|
11
|
+
* and `method_path()` in gen4_ac's `process_main.rs`, which spell the same
|
|
12
|
+
* derivation.
|
|
13
|
+
*
|
|
14
|
+
* Every method-lifecycle action therefore has to move both halves together:
|
|
15
|
+
*
|
|
16
|
+
* New — scaffold a blank procedure, or the method is unrunnable and
|
|
17
|
+
* Start refuses with "has no sequence on this machine".
|
|
18
|
+
* Duplicate — copy the procedure under the new id, or the copy silently runs
|
|
19
|
+
* nothing.
|
|
20
|
+
* Delete — remove the procedure, or an orphan file is left behind that a
|
|
21
|
+
* later method of the same name would inherit.
|
|
22
|
+
* Im/Export — carry the procedure, because a schema whose cycle columns
|
|
23
|
+
* nothing produces records blank rows forever.
|
|
24
|
+
*
|
|
25
|
+
* That last one is why import/export is a METHOD operation and not a sequence
|
|
26
|
+
* operation: half a method is not portable.
|
|
27
|
+
*
|
|
28
|
+
* The control program has no filesystem access, so all of this rides the
|
|
29
|
+
* datastore servelet over IPC, exactly as {@link useSequenceProducers} reads it.
|
|
30
|
+
*/
|
|
31
|
+
/** The datastore topic for a method's procedure. */
|
|
32
|
+
export declare function sequenceTopic(methodId: string): string;
|
|
33
|
+
/** A blank procedure, shaped so `validateMethod` and the editor accept it. */
|
|
34
|
+
export declare function emptySequence(methodId: string): Record<string, unknown>;
|
|
35
|
+
/** Is this datastore payload plausibly a `.seq.json` method? */
|
|
36
|
+
export declare function isSequenceShaped(v: any): boolean;
|
|
37
|
+
export interface UseMethodSequenceOptions {
|
|
38
|
+
/** Override the IPC layer (tests, playground). */
|
|
39
|
+
invoker?: (topic: string, messageType: number, payload: object) => Promise<any>;
|
|
40
|
+
}
|
|
41
|
+
export interface MethodSequenceIo {
|
|
42
|
+
/** The procedure for `methodId`, or null when there is none. */
|
|
43
|
+
read: (methodId: string) => Promise<any | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Write `sequence` as the procedure for `methodId`.
|
|
46
|
+
*
|
|
47
|
+
* `name` is rewritten to `methodId` on the way out, unconditionally. The
|
|
48
|
+
* method id owns the identity; the `name` inside the file is a leftover
|
|
49
|
+
* from when the sequence library named itself, and a copy that keeps its
|
|
50
|
+
* source's name is a file claiming to be a method it is not.
|
|
51
|
+
*/
|
|
52
|
+
write: (methodId: string, sequence: any) => Promise<void>;
|
|
53
|
+
/** Remove the procedure for `methodId`. Resolves even if there was none. */
|
|
54
|
+
remove: (methodId: string) => Promise<void>;
|
|
55
|
+
/** Copy `from`'s procedure to `to`. No-op when `from` has none. */
|
|
56
|
+
copy: (from: string, to: string) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export declare function useMethodSequence(options?: UseMethodSequenceOptions): MethodSequenceIo;
|
|
59
|
+
export default useMethodSequence;
|
|
60
|
+
//# sourceMappingURL=useMethodSequence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMethodSequence.d.ts","sourceRoot":"","sources":["../../src/hooks/useMethodSequence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAYH,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEvE;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAEhD;AAED,MAAM,WAAW,wBAAwB;IACrC,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACnF;AAED,MAAM,WAAW,gBAAgB;IAC7B,gEAAgE;IAChE,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,4EAA4E;IAC5E,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,gBAAgB,CA8CtF;AAED,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useCallback,useContext,useMemo}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";const METHOD_DIR="methods",METHOD_EXT=".seq.json";export function sequenceTopic(e){return`datastore.methods/${e}${METHOD_EXT}`}export function emptySequence(e){return{name:e,format_version:1,steps:[]}}export function isSequenceShaped(e){return!!e&&Array.isArray(e.steps)}export function useMethodSequence(e){const t=useContext(EventEmitterContext),n=e?.invoker??((e,n,s)=>t.invoke(e,n,s)),s=useCallback(async e=>{if(!e)return null;try{const t=await n(sequenceTopic(e),MessageType.Read,{});if(!1===t?.success)return null;const s=t?.data??t;return isSequenceShaped(s)?s:null}catch{return null}},[t]),o=useCallback(async(e,t)=>{const s={...t??{},name:e};await n(sequenceTopic(e),MessageType.Write,{value:s,options:{create_dirs:!0}})},[t]),a=useCallback(async e=>{try{await n(sequenceTopic(e),MessageType.Control,{action:"delete"})}catch{}},[t]),r=useCallback(async(e,t)=>{const n=await s(e);n&&await o(t,n)},[s,o]);return useMemo(()=>({read:s,write:o,remove:a,copy:r}),[s,o,a,r])}export default useMethodSequence;
|
package/package.json
CHANGED
|
@@ -839,9 +839,47 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
839
839
|
// Render
|
|
840
840
|
// -----------------------------------------------------------------
|
|
841
841
|
if (!projectId || !methodId || !runId || !schema) {
|
|
842
|
+
// Name what is missing, and offer the way out.
|
|
843
|
+
//
|
|
844
|
+
// "No test selected" was a dead end whenever it appeared while a test
|
|
845
|
+
// WAS open — which a mid-test page refresh could produce, because the
|
|
846
|
+
// selection is seeded asynchronously and an active run clears the
|
|
847
|
+
// History pins that would otherwise let the operator pick the run by
|
|
848
|
+
// hand. The operator was left with a screen that said nothing was
|
|
849
|
+
// running while the machine plainly was, and no control that changed
|
|
850
|
+
// it. Saying which piece is absent, and giving a button that re-points
|
|
851
|
+
// the view at whatever TIS says is active, means the state is always
|
|
852
|
+
// recoverable without closing the test.
|
|
853
|
+
const missing = [
|
|
854
|
+
!projectId && 'project',
|
|
855
|
+
!methodId && 'method',
|
|
856
|
+
!runId && 'run',
|
|
857
|
+
!schema && 'method definition',
|
|
858
|
+
].filter(Boolean).join(', ');
|
|
859
|
+
const canRecover = !!tis.state.active && !!tis.state.activeRunId;
|
|
842
860
|
return (
|
|
843
861
|
<div className="p-card" style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>
|
|
844
|
-
|
|
862
|
+
{canRecover ? (
|
|
863
|
+
<>
|
|
864
|
+
<div>
|
|
865
|
+
A test is running, but this view has not resolved it yet
|
|
866
|
+
{missing ? ` (no ${missing})` : ''}.
|
|
867
|
+
</div>
|
|
868
|
+
<Button
|
|
869
|
+
label="Show the active test"
|
|
870
|
+
icon="pi pi-refresh"
|
|
871
|
+
style={{ marginTop: '0.75rem' }}
|
|
872
|
+
onClick={() => tis.setSelection({
|
|
873
|
+
projectId: tis.state.activeProjectId || null,
|
|
874
|
+
methodId: tis.state.activeMethodId || null,
|
|
875
|
+
sampleId: tis.state.activeSampleId || null,
|
|
876
|
+
runId: tis.state.activeRunId || null,
|
|
877
|
+
})}
|
|
878
|
+
/>
|
|
879
|
+
</>
|
|
880
|
+
) : (
|
|
881
|
+
<>No test selected. Pick a row from the History tab or start a run.</>
|
|
882
|
+
)}
|
|
845
883
|
</div>
|
|
846
884
|
);
|
|
847
885
|
}
|
|
@@ -529,27 +529,72 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
529
529
|
{ fqdn: 'gm.tis_active_run_id', kind: 'active_run_id' },
|
|
530
530
|
];
|
|
531
531
|
let cancelled = false;
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
532
|
+
// Seed, and KEEP seeding until it works.
|
|
533
|
+
//
|
|
534
|
+
// Two bugs lived in the first version of this and both let a mid-test
|
|
535
|
+
// refresh land on an empty Data view with no way out:
|
|
536
|
+
//
|
|
537
|
+
// 1. `if (!isConnected) return` — `isConnected` is a FUNCTION, so it
|
|
538
|
+
// is always truthy and the guard never fired. It was calling for
|
|
539
|
+
// the connection and then not waiting for it.
|
|
540
|
+
// 2. `isConnected` was in the dep array as the "re-run when the
|
|
541
|
+
// connection lands" trigger, but it is a `useCallback` keyed on the
|
|
542
|
+
// hub, so its identity never changes. The effect ran exactly once,
|
|
543
|
+
// at mount, and whatever it got was final.
|
|
544
|
+
//
|
|
545
|
+
// Together those meant: one attempt, at the worst possible moment, with
|
|
546
|
+
// no retry. Now it runs when connected, re-runs on `HUB/connected`
|
|
547
|
+
// (published by HubWebSocket on socket open), and retries on a short
|
|
548
|
+
// timer until the read actually answers — because a read can also come
|
|
549
|
+
// back empty simply because the TIS module has not published yet.
|
|
550
|
+
//
|
|
551
|
+
// `seeded` latches on the first answer that carries a real value, so a
|
|
552
|
+
// machine with genuinely no test open stops retrying quickly rather
|
|
553
|
+
// than polling forever.
|
|
554
|
+
let seeded = false;
|
|
555
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
556
|
+
|
|
557
|
+
const seedOnce = async (): Promise<boolean> => {
|
|
558
|
+
if (!isConnected()) return false;
|
|
559
|
+
let sawValue = false;
|
|
538
560
|
for (const { fqdn, kind, bool } of SEED) {
|
|
539
561
|
try {
|
|
540
562
|
const resp: any = await read(fqdn);
|
|
541
|
-
if (cancelled) return;
|
|
563
|
+
if (cancelled) return true;
|
|
542
564
|
if (resp && resp.success === false) continue;
|
|
543
565
|
const v = resp?.data;
|
|
544
566
|
if (v === undefined || v === null) continue;
|
|
567
|
+
sawValue = true;
|
|
545
568
|
dispatch({ kind, value: bool ? !!v : String(v) } as StateAction);
|
|
546
569
|
} catch {
|
|
547
570
|
/* scalar absent on this machine — nothing to seed */
|
|
548
571
|
}
|
|
549
572
|
}
|
|
550
|
-
|
|
573
|
+
return sawValue;
|
|
574
|
+
};
|
|
551
575
|
|
|
552
|
-
|
|
576
|
+
const attempt = (delayMs: number) => {
|
|
577
|
+
if (cancelled || seeded) return;
|
|
578
|
+
void seedOnce().then((ok) => {
|
|
579
|
+
if (cancelled) return;
|
|
580
|
+
if (ok) { seeded = true; return; }
|
|
581
|
+
// Back off, but stay bounded: an operator who refreshes while
|
|
582
|
+
// the server is still coming up must not be left staring at a
|
|
583
|
+
// screen that will never fill in.
|
|
584
|
+
timer = setTimeout(() => attempt(Math.min(delayMs * 2, 5000)), delayMs);
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
attempt(250);
|
|
589
|
+
// A socket that opens after this effect mounted gets its own attempt.
|
|
590
|
+
const connSub = subscribe('HUB/connected', () => { seeded = false; attempt(250); });
|
|
591
|
+
|
|
592
|
+
return () => {
|
|
593
|
+
cancelled = true;
|
|
594
|
+
if (timer) clearTimeout(timer);
|
|
595
|
+
unsubscribe(connSub);
|
|
596
|
+
subs.forEach(unsubscribe);
|
|
597
|
+
};
|
|
553
598
|
}, [subscribe, unsubscribe, read, isConnected]);
|
|
554
599
|
|
|
555
600
|
// -----------------------------------------------------------------
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeleteMethodDialog — the three-step method deletion.
|
|
3
|
+
*
|
|
4
|
+
* Deleting a test method destroys more than a config entry. It takes the
|
|
5
|
+
* procedure that produced the numbers with it, and it orphans every run in
|
|
6
|
+
* history from the schema that gives those runs meaning: the column names, the
|
|
7
|
+
* units, the chart definitions. A `window.confirm` is not a proportionate gate
|
|
8
|
+
* on that.
|
|
9
|
+
*
|
|
10
|
+
* So the operator walks three steps and cannot skip any of them:
|
|
11
|
+
*
|
|
12
|
+
* 1. Read what goes. Named, counted, and explicit about the history.
|
|
13
|
+
* 2. Download the archive. MANDATORY, not advisory. `tis.export_method_zip`
|
|
14
|
+
* hands back every run of this method. The button
|
|
15
|
+
* gates step 3, so "I'll do it later" is not a
|
|
16
|
+
* path through this dialog.
|
|
17
|
+
* 3. Type the phrase. `YES I AM SURE`, exactly. A phrase cannot be
|
|
18
|
+
* hit by a mis-touch on a panel, and typing it is
|
|
19
|
+
* the moment the operator reads step 1 properly.
|
|
20
|
+
*
|
|
21
|
+
* The deletion itself is still STAGED — the TIS editor's Save is what persists
|
|
22
|
+
* it — but the sequence file and the operator's confidence are not, so this is
|
|
23
|
+
* where the friction belongs.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { useEffect, useState } from 'react';
|
|
27
|
+
import { Button } from 'primereact/button';
|
|
28
|
+
import { Dialog } from 'primereact/dialog';
|
|
29
|
+
import { InputText } from 'primereact/inputtext';
|
|
30
|
+
import { Message } from 'primereact/message';
|
|
31
|
+
|
|
32
|
+
/** Typed exactly, case-sensitive. */
|
|
33
|
+
export const DELETE_PHRASE = 'YES I AM SURE';
|
|
34
|
+
|
|
35
|
+
export interface DeleteMethodDialogProps {
|
|
36
|
+
visible: boolean;
|
|
37
|
+
methodId: string | null;
|
|
38
|
+
/** True when this method's procedure is a file that will also be removed. */
|
|
39
|
+
hasSequence: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the archive of this method's runs and hand it to the browser.
|
|
42
|
+
* Resolves true once the download has been started.
|
|
43
|
+
*/
|
|
44
|
+
onDownloadArchive: (methodId: string) => Promise<boolean>;
|
|
45
|
+
onConfirm: () => void;
|
|
46
|
+
onCancel: () => void;
|
|
47
|
+
busy?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DeleteMethodDialog: React.FC<DeleteMethodDialogProps> = ({
|
|
51
|
+
visible,
|
|
52
|
+
methodId,
|
|
53
|
+
hasSequence,
|
|
54
|
+
onDownloadArchive,
|
|
55
|
+
onConfirm,
|
|
56
|
+
onCancel,
|
|
57
|
+
busy,
|
|
58
|
+
}) => {
|
|
59
|
+
const [archived, setArchived] = useState(false);
|
|
60
|
+
const [downloading, setDownloading] = useState(false);
|
|
61
|
+
const [phrase, setPhrase] = useState('');
|
|
62
|
+
const [error, setError] = useState<string | null>(null);
|
|
63
|
+
|
|
64
|
+
// Reset every time the dialog opens, and key on the method: reusing the
|
|
65
|
+
// "already archived" flag across two different methods would let the second
|
|
66
|
+
// one out of step 2 on the strength of the first one's download.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (visible) {
|
|
69
|
+
setArchived(false);
|
|
70
|
+
setDownloading(false);
|
|
71
|
+
setPhrase('');
|
|
72
|
+
setError(null);
|
|
73
|
+
}
|
|
74
|
+
}, [visible, methodId]);
|
|
75
|
+
|
|
76
|
+
const doDownload = async () => {
|
|
77
|
+
if (!methodId) return;
|
|
78
|
+
setDownloading(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const ok = await onDownloadArchive(methodId);
|
|
82
|
+
setArchived(ok);
|
|
83
|
+
if (!ok) setError('The archive could not be built, so nothing has been deleted.');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
setError(`The archive could not be built, so nothing has been deleted: ${
|
|
86
|
+
e instanceof Error ? e.message : String(e)}`);
|
|
87
|
+
} finally {
|
|
88
|
+
setDownloading(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const phraseOk = phrase === DELETE_PHRASE;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Dialog
|
|
96
|
+
header={`Delete test method "${methodId ?? ''}"`}
|
|
97
|
+
visible={visible}
|
|
98
|
+
onHide={onCancel}
|
|
99
|
+
style={{ width: '34rem' }}
|
|
100
|
+
modal
|
|
101
|
+
>
|
|
102
|
+
<ol className="tis-editor__delete-steps">
|
|
103
|
+
<li>
|
|
104
|
+
<strong>What this removes</strong>
|
|
105
|
+
<ul>
|
|
106
|
+
<li>The method definition: its fields, charts, results and asset references.</li>
|
|
107
|
+
{hasSequence && <li>Its sequence — the procedure that runs the test.</li>}
|
|
108
|
+
<li>
|
|
109
|
+
Nothing in the recorded data is erased, but every past run of this
|
|
110
|
+
method loses the definition that names its columns. Reports and
|
|
111
|
+
charts for those runs will no longer render.
|
|
112
|
+
</li>
|
|
113
|
+
</ul>
|
|
114
|
+
</li>
|
|
115
|
+
|
|
116
|
+
<li>
|
|
117
|
+
<strong>Download the archive</strong>
|
|
118
|
+
<p>
|
|
119
|
+
Every run recorded under this method, as a ZIP. This is required — the
|
|
120
|
+
confirmation below stays locked until the download has started.
|
|
121
|
+
</p>
|
|
122
|
+
<Button
|
|
123
|
+
label={archived ? 'Archive downloaded' : 'Download archive'}
|
|
124
|
+
icon={downloading ? 'pi pi-spin pi-spinner' : archived ? 'pi pi-check' : 'pi pi-box'}
|
|
125
|
+
severity={archived ? 'success' : undefined}
|
|
126
|
+
disabled={downloading || busy}
|
|
127
|
+
onClick={doDownload}
|
|
128
|
+
/>
|
|
129
|
+
</li>
|
|
130
|
+
|
|
131
|
+
<li>
|
|
132
|
+
<strong>Confirm</strong>
|
|
133
|
+
<p>
|
|
134
|
+
Type <code>{DELETE_PHRASE}</code> to enable the delete button.
|
|
135
|
+
</p>
|
|
136
|
+
<InputText
|
|
137
|
+
value={phrase}
|
|
138
|
+
onChange={(e) => setPhrase(e.target.value)}
|
|
139
|
+
placeholder={DELETE_PHRASE}
|
|
140
|
+
disabled={!archived || busy}
|
|
141
|
+
style={{ width: '100%' }}
|
|
142
|
+
/>
|
|
143
|
+
</li>
|
|
144
|
+
</ol>
|
|
145
|
+
|
|
146
|
+
{error && <Message severity="error" text={error} style={{ width: '100%' }} />}
|
|
147
|
+
|
|
148
|
+
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
|
|
149
|
+
<Button label="Cancel" className="p-button-text" onClick={onCancel} disabled={busy} />
|
|
150
|
+
<Button
|
|
151
|
+
label="Delete method"
|
|
152
|
+
icon="pi pi-trash"
|
|
153
|
+
className="p-button-danger"
|
|
154
|
+
disabled={!archived || !phraseOk || busy}
|
|
155
|
+
onClick={onConfirm}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</Dialog>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export default DeleteMethodDialog;
|