@adcops/autocore-react 3.5.15 → 3.6.3
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-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-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
|
@@ -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;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MethodTransfer — a whole test method as one portable file.
|
|
3
|
+
*
|
|
4
|
+
* ## Why the method and not the sequence
|
|
5
|
+
*
|
|
6
|
+
* A sequence-backed method is two halves with a contract between them: the
|
|
7
|
+
* schema declares the cycle columns, and the procedure's `store_value` steps
|
|
8
|
+
* fill them by name. Exporting either half alone produces something that looks
|
|
9
|
+
* like a method and silently is not — a schema with no producer records blank
|
|
10
|
+
* rows on every cycle forever, and a procedure with no schema stores values
|
|
11
|
+
* nothing displays. Neither failure announces itself.
|
|
12
|
+
*
|
|
13
|
+
* So the exportable unit is the pair. This is the same reasoning that made
|
|
14
|
+
* `acctl push methods` move both files with one verb.
|
|
15
|
+
*
|
|
16
|
+
* ## The file
|
|
17
|
+
*
|
|
18
|
+
* Plain JSON, human-readable, no archive container: it goes on a USB stick
|
|
19
|
+
* between companies and someone will open it in a text editor.
|
|
20
|
+
*
|
|
21
|
+
* {
|
|
22
|
+
* "format": "autocore-method",
|
|
23
|
+
* "version": 1,
|
|
24
|
+
* "method_id": "golf_compression",
|
|
25
|
+
* "schema": { ...test_methods.json entry... },
|
|
26
|
+
* "sequence": { ...seq.json..., or null for a control-backed method }
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* `method_id` is a SUGGESTION, not an instruction. The importer offers it and
|
|
30
|
+
* lets the operator rename, because a method list that fills up with
|
|
31
|
+
* `golf_compression_copy_3` is the complaint that follows otherwise.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { TestMethod } from './types';
|
|
35
|
+
|
|
36
|
+
export const METHOD_FILE_FORMAT = 'autocore-method';
|
|
37
|
+
export const METHOD_FILE_VERSION = 1;
|
|
38
|
+
|
|
39
|
+
export interface MethodTransferFile {
|
|
40
|
+
format: typeof METHOD_FILE_FORMAT;
|
|
41
|
+
version: number;
|
|
42
|
+
method_id: string;
|
|
43
|
+
schema: TestMethod;
|
|
44
|
+
/** The procedure, or null for a control-backed method. */
|
|
45
|
+
sequence: any | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build the export payload. */
|
|
49
|
+
export function buildTransfer(
|
|
50
|
+
methodId: string,
|
|
51
|
+
schema: TestMethod,
|
|
52
|
+
sequence: any | null,
|
|
53
|
+
): MethodTransferFile {
|
|
54
|
+
return {
|
|
55
|
+
format: METHOD_FILE_FORMAT,
|
|
56
|
+
version: METHOD_FILE_VERSION,
|
|
57
|
+
method_id: methodId,
|
|
58
|
+
schema,
|
|
59
|
+
sequence: sequence ?? null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Legal method ids, and why the rule is this strict.
|
|
65
|
+
*
|
|
66
|
+
* The id becomes a path segment (`methods/<id>.seq.json`) and a directory name
|
|
67
|
+
* under the project (`<project>/<method_id>/<run_id>/`), both joined
|
|
68
|
+
* server-side, so anything that could traverse or quote-escape is refused at
|
|
69
|
+
* entry rather than sanitised silently — an id that comes back different from
|
|
70
|
+
* what was typed is how you end up with two methods you believe are one.
|
|
71
|
+
*
|
|
72
|
+
* It is also a Rust identifier stem in generated code, hence the leading-letter
|
|
73
|
+
* rule: `2point_bend` would emit `2pointBendTestManager`, which does not
|
|
74
|
+
* compile.
|
|
75
|
+
*/
|
|
76
|
+
export const METHOD_ID_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
77
|
+
export const METHOD_ID_RULE =
|
|
78
|
+
'Letters, digits and underscore only; must start with a letter.';
|
|
79
|
+
|
|
80
|
+
/** Parse and validate an imported file. Returns the file or a reason. */
|
|
81
|
+
export function parseTransfer(text: string): { file: MethodTransferFile } | { error: string } {
|
|
82
|
+
let parsed: unknown;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(text);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return { error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
87
|
+
}
|
|
88
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
89
|
+
return { error: 'File does not contain a method object.' };
|
|
90
|
+
}
|
|
91
|
+
const f = parsed as Partial<MethodTransferFile>;
|
|
92
|
+
if (f.format !== METHOD_FILE_FORMAT) {
|
|
93
|
+
// Name what it looks like. A `.seq.json` dropped here is the most
|
|
94
|
+
// likely wrong file, and "unrecognised format" would leave the operator
|
|
95
|
+
// with no idea which of their two files to reach for.
|
|
96
|
+
const looksLikeSequence = Array.isArray((parsed as any)?.steps);
|
|
97
|
+
return {
|
|
98
|
+
error: looksLikeSequence
|
|
99
|
+
? 'This is a sequence file, not a whole method. Import it from a method export, '
|
|
100
|
+
+ 'or create the method first and edit its sequence on the Method screen.'
|
|
101
|
+
: 'Not an autocore method export (missing "format": "autocore-method").',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (typeof f.version !== 'number' || f.version > METHOD_FILE_VERSION) {
|
|
105
|
+
return {
|
|
106
|
+
error: `This file was written by a newer version (v${f.version}); `
|
|
107
|
+
+ `this machine understands up to v${METHOD_FILE_VERSION}.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (typeof f.method_id !== 'string' || !METHOD_ID_RE.test(f.method_id)) {
|
|
111
|
+
return { error: `File has no usable method id. ${METHOD_ID_RULE}` };
|
|
112
|
+
}
|
|
113
|
+
if (typeof f.schema !== 'object' || f.schema === null) {
|
|
114
|
+
return { error: 'File has no method schema.' };
|
|
115
|
+
}
|
|
116
|
+
// A sequence-backed method with no procedure in the file is a half export;
|
|
117
|
+
// refuse it rather than importing a method that cannot run.
|
|
118
|
+
if ((f.schema as TestMethod).procedure === 'sequence' && !Array.isArray(f.sequence?.steps)) {
|
|
119
|
+
return {
|
|
120
|
+
error: 'This method declares a sequence procedure but the file carries no sequence. '
|
|
121
|
+
+ 'Re-export it from the machine it came from.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { file: f as MethodTransferFile };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Hand the browser a method export to save. */
|
|
128
|
+
export function downloadTransfer(file: MethodTransferFile): void {
|
|
129
|
+
const blob = new Blob([JSON.stringify(file, null, 2)], { type: 'application/json' });
|
|
130
|
+
const url = URL.createObjectURL(blob);
|
|
131
|
+
const a = document.createElement('a');
|
|
132
|
+
a.href = url;
|
|
133
|
+
a.download = `${file.method_id}.method.json`;
|
|
134
|
+
a.click();
|
|
135
|
+
// Revoke on the next tick: revoking synchronously can beat the download in
|
|
136
|
+
// Chromium, which is the browser on every panel.
|
|
137
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
138
|
+
}
|
|
@@ -150,3 +150,34 @@
|
|
|
150
150
|
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* The three-step method deletion. Numbered because the steps are ordered and
|
|
156
|
+
* gated: the archive download unlocks the confirmation phrase, which unlocks
|
|
157
|
+
* the button. See DeleteMethodDialog for why deleting a method earns this.
|
|
158
|
+
*/
|
|
159
|
+
.tis-editor__delete-steps {
|
|
160
|
+
margin: 0 0 1rem;
|
|
161
|
+
padding-left: 1.4rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.tis-editor__delete-steps > li {
|
|
165
|
+
margin-bottom: 1rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.tis-editor__delete-steps p {
|
|
169
|
+
margin: 0.35rem 0 0.5rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.tis-editor__delete-steps ul {
|
|
173
|
+
margin: 0.35rem 0 0;
|
|
174
|
+
padding-left: 1.1rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.tis-editor__delete-steps code {
|
|
178
|
+
padding: 0.1rem 0.35rem;
|
|
179
|
+
border-radius: 3px;
|
|
180
|
+
background: var(--surface-ground);
|
|
181
|
+
font-family: var(--font-family-mono, monospace);
|
|
182
|
+
letter-spacing: 0.04em;
|
|
183
|
+
}
|