@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.
Files changed (39) hide show
  1. package/dist/components/tis-editor/DeleteMethodDialog.d.ts +43 -0
  2. package/dist/components/tis-editor/DeleteMethodDialog.d.ts.map +1 -0
  3. package/dist/components/tis-editor/DeleteMethodDialog.js +1 -0
  4. package/dist/components/tis-editor/MethodTransfer.d.ts +69 -0
  5. package/dist/components/tis-editor/MethodTransfer.d.ts.map +1 -0
  6. package/dist/components/tis-editor/MethodTransfer.js +1 -0
  7. package/dist/components/tis-editor/TisConfigEditor.css +31 -0
  8. package/dist/components/tis-editor/TisConfigEditor.d.ts +9 -1
  9. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  10. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  11. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -1
  12. package/dist/components/tis-editor/editor/IdentitySection.js +1 -1
  13. package/dist/components/tis-editor/types.d.ts +11 -0
  14. package/dist/components/tis-editor/types.d.ts.map +1 -1
  15. package/dist/components/varpick/VariablePicker.d.ts.map +1 -1
  16. package/dist/components/varpick/VariablePicker.js +1 -1
  17. package/dist/components/varpick/varpick.css +84 -30
  18. package/dist/hooks/index.d.ts +2 -0
  19. package/dist/hooks/index.d.ts.map +1 -1
  20. package/dist/hooks/index.js +1 -1
  21. package/dist/hooks/useMethodSequence.d.ts +60 -0
  22. package/dist/hooks/useMethodSequence.d.ts.map +1 -0
  23. package/dist/hooks/useMethodSequence.js +1 -0
  24. package/package.json +1 -1
  25. package/src/components/tis-editor/DeleteMethodDialog.tsx +162 -0
  26. package/src/components/tis-editor/MethodTransfer.ts +138 -0
  27. package/src/components/tis-editor/TisConfigEditor.css +31 -0
  28. package/src/components/tis-editor/TisConfigEditor.tsx +305 -7
  29. package/src/components/tis-editor/editor/IdentitySection.tsx +33 -1
  30. package/src/components/tis-editor/types.ts +11 -0
  31. package/src/components/varpick/VariablePicker.tsx +4 -0
  32. package/src/components/varpick/varpick.css +84 -30
  33. package/src/hooks/index.ts +4 -0
  34. package/src/hooks/useMethodSequence.ts +128 -0
  35. package/tools/tests/dist/{TestFieldDialog-BvHAr2GQ.js → TestFieldDialog-Dph2oPfe.js} +1 -0
  36. package/tools/tests/dist/autocore-react.css +84 -30
  37. package/tools/tests/dist/tis-editor.entry.js +1 -1
  38. package/tools/tests/dist/varpick.entry.js +1 -1
  39. 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
+ }