@adcops/autocore-react 3.3.57 → 3.3.61
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 +6 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ProjectInfoDialog.d.ts +21 -0
- package/dist/components/tis/ProjectInfoDialog.d.ts.map +1 -0
- package/dist/components/tis/ProjectInfoDialog.js +1 -0
- package/dist/components/tis/ProjectSelector.d.ts +15 -0
- package/dist/components/tis/ProjectSelector.d.ts.map +1 -0
- package/dist/components/tis/ProjectSelector.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +4 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestMethodDialog.d.ts +17 -0
- package/dist/components/tis/TestMethodDialog.d.ts.map +1 -0
- package/dist/components/tis/TestMethodDialog.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +19 -10
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +28 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +9 -0
- package/src/components/tis/ProjectInfoDialog.tsx +307 -0
- package/src/components/tis/ProjectSelector.tsx +190 -0
- package/src/components/tis/TestDataView.tsx +4 -0
- package/src/components/tis/TestMethodDialog.tsx +147 -0
- package/src/components/tis/TestSetupForm.tsx +167 -116
- package/src/components/tis/TisProvider.tsx +148 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { Button } from 'primereact/button';
|
|
3
|
+
import { InputText } from 'primereact/inputtext';
|
|
4
|
+
import { Tooltip } from 'primereact/tooltip';
|
|
5
5
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
6
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
7
7
|
import { MessageType } from '../../hub/CommandMessage';
|
|
8
8
|
import { ValueInput } from '../ValueInput';
|
|
9
9
|
import { TextInput } from '../TextInput';
|
|
10
10
|
import { useTis } from './TisProvider';
|
|
11
|
+
import { TestMethodDialog } from './TestMethodDialog';
|
|
11
12
|
|
|
12
13
|
export interface TestFieldDef {
|
|
14
|
+
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
13
15
|
name: string;
|
|
14
16
|
type: string;
|
|
15
17
|
units?: string;
|
|
16
18
|
required?: boolean;
|
|
17
19
|
source?: string;
|
|
20
|
+
/** Pretty label rendered by the form. Falls back to `name`. Units
|
|
21
|
+
* are appended automatically; don't pre-format `[mm]` into label. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
24
|
+
description?: string;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export interface TestMethod {
|
|
@@ -22,33 +29,51 @@ export interface TestMethod {
|
|
|
22
29
|
config_fields: TestFieldDef[];
|
|
23
30
|
cycle_fields: TestFieldDef[];
|
|
24
31
|
results_fields: TestFieldDef[];
|
|
32
|
+
/** Optional pretty label for the Test Method picker. Falls back
|
|
33
|
+
* to the canonical method_id key. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Optional long-form description shown in the picker dialog
|
|
36
|
+
* when this method is highlighted. */
|
|
37
|
+
description?: string;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
41
|
+
* Test-setup form. Renders Sample ID, Test Method picker, and Test
|
|
42
|
+
* Configuration. Project ID lives in `<ProjectSelector>` on its own
|
|
43
|
+
* tab — this form reads the selected project from `<TisProvider>`
|
|
44
|
+
* and gates staging on it being a known project (created via the
|
|
45
|
+
* Project tab's `+` button).
|
|
31
46
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* state when the corresponding TIS-context fields are blank.
|
|
35
|
-
* - `onProjectChange` / `onMethodChange` / `onValidationChange`:
|
|
36
|
-
* receive callbacks in addition to the provider's selection state.
|
|
47
|
+
* All props are optional overrides — by default the form drives
|
|
48
|
+
* itself from the surrounding `<TisProvider>`.
|
|
37
49
|
*/
|
|
38
50
|
export interface TestSetupFormProps {
|
|
39
51
|
schema?: TestMethod;
|
|
40
|
-
defaultProjectId?: string;
|
|
41
52
|
defaultMethodId?: string;
|
|
42
|
-
onProjectChange?: (projectId: string) => void;
|
|
43
53
|
onMethodChange?: (methodId: string) => void;
|
|
44
54
|
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Helpers
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
const labelOf = (f: TestFieldDef): string => {
|
|
62
|
+
const base = f.label && f.label.length > 0 ? f.label : f.name;
|
|
63
|
+
return f.units ? `${base} [${f.units}]` : base;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const hasDescription = (f: TestFieldDef): boolean =>
|
|
67
|
+
typeof f.description === 'string' && f.description.length > 0;
|
|
68
|
+
|
|
69
|
+
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
70
|
+
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
71
|
+
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
|
|
47
74
|
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
48
75
|
schema: schemaOverride,
|
|
49
|
-
defaultProjectId,
|
|
50
76
|
defaultMethodId,
|
|
51
|
-
onProjectChange,
|
|
52
77
|
onMethodChange,
|
|
53
78
|
onValidationChange,
|
|
54
79
|
}) => {
|
|
@@ -58,33 +83,20 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
58
83
|
|
|
59
84
|
const methodIds = useMemo(() => Object.keys(tis.schemas), [tis.schemas]);
|
|
60
85
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
// The form owns Sample ID, Method, and per-test config_fields
|
|
87
|
+
// values. Project ID is sourced from the provider's selection
|
|
88
|
+
// (set by <ProjectSelector> on the Project tab).
|
|
89
|
+
const projectId = tis.selection.projectId;
|
|
90
|
+
const projectExists = projectId.trim() !== '' && tis.projectKnown(projectId.trim());
|
|
91
|
+
|
|
67
92
|
const [methodId, setMethodIdLocal] = useState<string>(
|
|
68
93
|
tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
|
|
69
94
|
);
|
|
70
95
|
const [sampleId, setSampleIdLocal] = useState<string>('');
|
|
71
96
|
const [config, setConfig] = useState<any>({});
|
|
72
97
|
|
|
73
|
-
// Resolve the schema for the active method. The override beats the
|
|
74
|
-
// registry; if neither is available, render the empty state.
|
|
75
98
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
76
99
|
|
|
77
|
-
// Push local edits back into the provider's selection so other
|
|
78
|
-
// components react. Only push when the value actually differs to
|
|
79
|
-
// avoid feedback loops.
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (tis.selection.projectId !== projectId) {
|
|
82
|
-
tis.setSelection({ projectId });
|
|
83
|
-
}
|
|
84
|
-
if (onProjectChange) onProjectChange(projectId);
|
|
85
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
-
}, [projectId]);
|
|
87
|
-
|
|
88
100
|
useEffect(() => {
|
|
89
101
|
if (tis.selection.methodId !== methodId && methodId) {
|
|
90
102
|
tis.setSelection({ methodId });
|
|
@@ -100,10 +112,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
100
112
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
113
|
}, [sampleId]);
|
|
102
114
|
|
|
103
|
-
// Track the live broadcast scalars — when the form is staged for
|
|
104
|
-
// a different sample externally (e.g., the operator types in
|
|
105
|
-
// another tab), reflect that here. Only react on a true change
|
|
106
|
-
// to avoid stomping local edits.
|
|
107
115
|
useEffect(() => {
|
|
108
116
|
if (tis.state.stagedSampleId && tis.state.stagedSampleId !== sampleId) {
|
|
109
117
|
setSampleIdLocal(tis.state.stagedSampleId);
|
|
@@ -111,18 +119,26 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
111
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
120
|
}, [tis.state.stagedSampleId]);
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
// If the provider's selected method changes elsewhere (e.g., the
|
|
123
|
+
// operator picks a different method via the dialog), reflect it
|
|
124
|
+
// here so the schema we render stays in sync.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (tis.selection.methodId && tis.selection.methodId !== methodId) {
|
|
127
|
+
setMethodIdLocal(tis.selection.methodId);
|
|
128
|
+
}
|
|
129
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
|
+
}, [tis.selection.methodId]);
|
|
131
|
+
|
|
116
132
|
const [isValid, setIsValid] = useState(false);
|
|
133
|
+
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
117
134
|
|
|
118
|
-
// Seed and live-update
|
|
135
|
+
// Seed and live-update config_fields that declare a `source`.
|
|
119
136
|
useEffect(() => {
|
|
120
137
|
if (!schema) return;
|
|
121
|
-
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
122
138
|
setConfig((prev: any) => {
|
|
123
139
|
let next = prev;
|
|
124
|
-
for (const field of
|
|
125
|
-
if (field.name === 'sample_id') continue;
|
|
140
|
+
for (const field of schema.config_fields) {
|
|
141
|
+
if (field.name === 'sample_id') continue;
|
|
126
142
|
if (!field.source) continue;
|
|
127
143
|
const tag = findTagByFqdn(field.source);
|
|
128
144
|
if (!tag) continue;
|
|
@@ -136,61 +152,53 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
136
152
|
});
|
|
137
153
|
}, [schema, rawValues, findTagByFqdn]);
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
setExistingProjects(resp.data.projects);
|
|
145
|
-
}
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error('Failed to list projects', err);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
fetchProjects();
|
|
151
|
-
}, [invoke]);
|
|
152
|
-
|
|
153
|
-
const searchProjects = (event: AutoCompleteCompleteEvent) => {
|
|
154
|
-
const query = event.query.toLowerCase();
|
|
155
|
-
setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Validation drives both the local UI and the auto-stage. We only
|
|
159
|
-
// call `tis.stage_test` when every required piece is present —
|
|
160
|
-
// otherwise the operator's edge-triggered Start button could fire
|
|
161
|
-
// a half-baked stage.
|
|
155
|
+
// Validation: project must be known (set on Project tab + + button),
|
|
156
|
+
// sample_id non-empty, and every required config_field present. We
|
|
157
|
+
// also require the provider's projectFieldsCache for the selected
|
|
158
|
+
// project to be loaded before we stage — otherwise the recorded
|
|
159
|
+
// test.json would be missing project-level metadata.
|
|
162
160
|
useEffect(() => {
|
|
163
161
|
if (!schema) { setIsValid(false); return; }
|
|
164
162
|
let valid = true;
|
|
165
|
-
if (!
|
|
163
|
+
if (!projectExists) valid = false;
|
|
166
164
|
if (!methodId.trim()) valid = false;
|
|
167
165
|
if (!sampleId.trim()) valid = false;
|
|
166
|
+
if (valid && !tis.projectFieldsLoaded) valid = false;
|
|
168
167
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
if (field.name === 'sample_id') continue; // tracked separately
|
|
168
|
+
for (const field of schema.config_fields) {
|
|
169
|
+
if (field.name === 'sample_id') continue;
|
|
172
170
|
if (field.required) {
|
|
173
171
|
const v = config[field.name];
|
|
174
172
|
if (v === undefined || v === '' || v === null) { valid = false; break; }
|
|
175
173
|
}
|
|
176
174
|
}
|
|
175
|
+
|
|
177
176
|
setIsValid(valid);
|
|
178
177
|
if (onValidationChange) onValidationChange(valid, config);
|
|
179
178
|
|
|
180
|
-
// Auto-stage when everything is valid. Stripping sample_id from
|
|
181
|
-
// config (it's a top-level peer of project_id/method_id now;
|
|
182
|
-
// the legacy nested form is server-side hoisted with a
|
|
183
|
-
// deprecation warning, but new code shouldn't rely on it).
|
|
184
179
|
if (valid) {
|
|
185
|
-
const { sample_id: _drop, ...
|
|
180
|
+
const { sample_id: _drop, ...configRest } = (config ?? {}) as any;
|
|
181
|
+
// Combine persisted project_fields (managerial setup) with
|
|
182
|
+
// per-test config_fields. If keys collide the per-test
|
|
183
|
+
// value wins — operators are closer to the run than the
|
|
184
|
+
// project metadata.
|
|
185
|
+
const mergedConfig = {
|
|
186
|
+
...tis.projectFields,
|
|
187
|
+
...configRest,
|
|
188
|
+
};
|
|
186
189
|
void invoke('tis.stage_test' as any, MessageType.Request, {
|
|
187
190
|
project_id: projectId,
|
|
188
191
|
method_id: methodId,
|
|
189
192
|
sample_id: sampleId,
|
|
190
|
-
config:
|
|
193
|
+
config: mergedConfig,
|
|
191
194
|
} as any).catch(e => console.error('[TestSetupForm] stage_test failed:', e));
|
|
192
195
|
}
|
|
193
|
-
|
|
196
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
197
|
+
}, [
|
|
198
|
+
config, schema, projectId, methodId, sampleId,
|
|
199
|
+
projectExists, tis.projectFields, tis.projectFieldsLoaded,
|
|
200
|
+
onValidationChange, invoke,
|
|
201
|
+
]);
|
|
194
202
|
|
|
195
203
|
const isFieldValid = (field: TestFieldDef) => {
|
|
196
204
|
if (!field.required) return true;
|
|
@@ -198,11 +206,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
198
206
|
return v !== undefined && v !== '' && v !== null;
|
|
199
207
|
};
|
|
200
208
|
|
|
201
|
-
const handleProjectIdChange = (value: string | null | undefined) => {
|
|
202
|
-
const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
|
|
203
|
-
setProjectIdLocal(sanitized);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
209
|
const handleSampleIdChange = (value: string) => {
|
|
207
210
|
setSampleIdLocal(value);
|
|
208
211
|
};
|
|
@@ -215,13 +218,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
215
218
|
}
|
|
216
219
|
};
|
|
217
220
|
|
|
218
|
-
const
|
|
221
|
+
const renderConfigField = (field: TestFieldDef) => {
|
|
219
222
|
if (field.name === 'sample_id') return null;
|
|
220
223
|
const valid = isFieldValid(field);
|
|
221
224
|
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
225
|
+
const tooltipId = `acFormInfo_${field.name}`;
|
|
222
226
|
return (
|
|
223
227
|
<React.Fragment key={field.name}>
|
|
224
|
-
<span className="ac-form-label">{field
|
|
228
|
+
<span className="ac-form-label">{labelOf(field)}</span>
|
|
225
229
|
{isNum ? (
|
|
226
230
|
<ValueInput
|
|
227
231
|
label={undefined}
|
|
@@ -237,7 +241,20 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
237
241
|
className={!valid ? 'p-invalid' : ''}
|
|
238
242
|
/>
|
|
239
243
|
)}
|
|
240
|
-
|
|
244
|
+
{hasDescription(field) ? (
|
|
245
|
+
<>
|
|
246
|
+
<Tooltip target={`#${tooltipId}`} position="left" />
|
|
247
|
+
<span
|
|
248
|
+
id={tooltipId}
|
|
249
|
+
data-pr-tooltip={field.description}
|
|
250
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'help' }}
|
|
251
|
+
>
|
|
252
|
+
<i className="pi pi-info-circle" style={{ color: 'var(--text-secondary-color)' }} />
|
|
253
|
+
</span>
|
|
254
|
+
</>
|
|
255
|
+
) : (
|
|
256
|
+
<span aria-hidden="true" />
|
|
257
|
+
)}
|
|
241
258
|
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
242
259
|
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
243
260
|
</span>
|
|
@@ -255,30 +272,46 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
255
272
|
);
|
|
256
273
|
}
|
|
257
274
|
|
|
275
|
+
// Cross-tab guard: if no project is selected (or the typed name
|
|
276
|
+
// hasn't been created yet on the Project tab), the form is
|
|
277
|
+
// effectively useless because every staging path needs a real
|
|
278
|
+
// project_id. Render an explicit empty-state pointing the user
|
|
279
|
+
// back to the Project tab so they don't sit confused in front of
|
|
280
|
+
// a half-functional form.
|
|
281
|
+
if (!projectExists) {
|
|
282
|
+
return (
|
|
283
|
+
<div style={{ padding: '1.25rem', maxWidth: '600px' }}>
|
|
284
|
+
<h3 className="ac-form-section">No project selected</h3>
|
|
285
|
+
<p style={{ color: 'var(--text-secondary-color)', marginTop: '0.5rem' }}>
|
|
286
|
+
Pick a project on the <strong>Project</strong> tab first
|
|
287
|
+
{projectId.trim() !== '' && ` (or click + there to create "${projectId.trim()}")`}.
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const gridStyle: React.CSSProperties = {
|
|
294
|
+
padding: '1.25rem',
|
|
295
|
+
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
296
|
+
};
|
|
297
|
+
|
|
258
298
|
return (
|
|
259
|
-
<div className="ac-form-grid" style={
|
|
299
|
+
<div className="ac-form-grid" style={gridStyle}>
|
|
260
300
|
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
261
|
-
|
|
301
|
+
Test Setup
|
|
262
302
|
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
263
303
|
<i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
|
|
264
304
|
</span>
|
|
305
|
+
<span style={{
|
|
306
|
+
fontSize: '0.85em',
|
|
307
|
+
color: 'var(--text-secondary-color)',
|
|
308
|
+
fontWeight: 'normal',
|
|
309
|
+
marginLeft: '0.25rem',
|
|
310
|
+
}}>
|
|
311
|
+
project: <strong>{projectId}</strong>
|
|
312
|
+
</span>
|
|
265
313
|
</h3>
|
|
266
314
|
|
|
267
|
-
<span className="ac-form-label">Project ID</span>
|
|
268
|
-
<AutoComplete
|
|
269
|
-
value={projectId}
|
|
270
|
-
suggestions={filteredProjects}
|
|
271
|
-
completeMethod={searchProjects}
|
|
272
|
-
onChange={(e) => handleProjectIdChange(e.value)}
|
|
273
|
-
dropdown
|
|
274
|
-
placeholder="Enter or select Project ID"
|
|
275
|
-
className={!projectId.trim() ? 'p-invalid' : ''}
|
|
276
|
-
/>
|
|
277
|
-
<span />
|
|
278
|
-
<span style={{ color: projectId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
279
|
-
<i className={projectId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
280
|
-
</span>
|
|
281
|
-
|
|
282
315
|
<span className="ac-form-label">Sample ID</span>
|
|
283
316
|
<TextInput
|
|
284
317
|
label={undefined}
|
|
@@ -286,29 +319,47 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
286
319
|
onValueChanged={handleSampleIdChange}
|
|
287
320
|
className={!sampleId.trim() ? 'p-invalid' : ''}
|
|
288
321
|
/>
|
|
289
|
-
<span />
|
|
322
|
+
<span aria-hidden="true" />
|
|
290
323
|
<span style={{ color: sampleId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
291
324
|
<i className={sampleId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
292
325
|
</span>
|
|
293
326
|
|
|
294
|
-
{methodIds.length >
|
|
327
|
+
{methodIds.length > 0 && (
|
|
295
328
|
<>
|
|
296
329
|
<span className="ac-form-label">Test Method</span>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
330
|
+
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
331
|
+
<InputText
|
|
332
|
+
value={methodLabelOf(methodId, schema)}
|
|
333
|
+
readOnly
|
|
334
|
+
style={{ flex: 1 }}
|
|
335
|
+
tabIndex={-1}
|
|
336
|
+
/>
|
|
337
|
+
<Button
|
|
338
|
+
icon="pi pi-pencil"
|
|
339
|
+
type="button"
|
|
340
|
+
onClick={() => setMethodPickerOpen(true)}
|
|
341
|
+
tooltip={methodIds.length > 1
|
|
342
|
+
? 'Change test method'
|
|
343
|
+
: 'View test method details'}
|
|
344
|
+
tooltipOptions={{ position: 'top' }}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
<span aria-hidden="true" />
|
|
348
|
+
<span style={{ color: methodId ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
349
|
+
<i className={methodId ? 'pi pi-check' : 'pi pi-times'} />
|
|
350
|
+
</span>
|
|
304
351
|
</>
|
|
305
352
|
)}
|
|
306
353
|
|
|
307
|
-
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
|
|
308
|
-
{schema.project_fields.map(renderField)}
|
|
309
|
-
|
|
310
354
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
311
|
-
{schema.config_fields.map(
|
|
355
|
+
{schema.config_fields.map(renderConfigField)}
|
|
356
|
+
|
|
357
|
+
<TestMethodDialog
|
|
358
|
+
visible={methodPickerOpen}
|
|
359
|
+
onHide={() => setMethodPickerOpen(false)}
|
|
360
|
+
currentMethodId={methodId}
|
|
361
|
+
onSelected={(picked) => setMethodIdLocal(picked)}
|
|
362
|
+
/>
|
|
312
363
|
</div>
|
|
313
364
|
);
|
|
314
365
|
};
|
|
@@ -90,6 +90,45 @@ export interface TisContextValue {
|
|
|
90
90
|
selection: TisSelection;
|
|
91
91
|
setSelection: (patch: TisSelectionPatch) => void;
|
|
92
92
|
|
|
93
|
+
// -----------------------------------------------------------------
|
|
94
|
+
// Project management — used by both <ProjectSelector> (Project tab)
|
|
95
|
+
// and <TestSetupForm> (Test tab) so they share a single source of
|
|
96
|
+
// truth for "which projects are real" and "what fields does the
|
|
97
|
+
// current one have." Keeping this in the provider rather than in
|
|
98
|
+
// TestSetupForm lets the two components live in different tabs
|
|
99
|
+
// without prop-threading.
|
|
100
|
+
// -----------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/** Project IDs returned by the server's `tis.list_projects`. */
|
|
103
|
+
existingProjects: string[];
|
|
104
|
+
/** True when the project either exists on disk OR was created in
|
|
105
|
+
* this browser session via `<ProjectInfoDialog mode="create">`.
|
|
106
|
+
* This is the gate for staging — typing an unknown name is
|
|
107
|
+
* invalid until + creates the directory. */
|
|
108
|
+
projectKnown: (id: string) => boolean;
|
|
109
|
+
/** Refresh `existingProjects` from the server. Called automatically
|
|
110
|
+
* on `tis.project_created` / `tis.project_updated` broadcasts. */
|
|
111
|
+
refreshProjects: () => Promise<void>;
|
|
112
|
+
/** Add a project ID to the in-session "just created" set so the
|
|
113
|
+
* form is immediately valid for it without round-tripping to
|
|
114
|
+
* list_projects. Idempotent. */
|
|
115
|
+
markProjectJustCreated: (id: string) => void;
|
|
116
|
+
|
|
117
|
+
/** `project_fields` blob for the currently-selected project,
|
|
118
|
+
* fetched from project.json. `{}` when nothing is loaded yet
|
|
119
|
+
* (use `projectFieldsLoaded` to disambiguate "empty project" vs
|
|
120
|
+
* "still fetching"). */
|
|
121
|
+
projectFields: Record<string, any>;
|
|
122
|
+
projectFieldsLoaded: boolean;
|
|
123
|
+
/** Fetch and cache project_fields for one project. Returns the
|
|
124
|
+
* fields on success, or null on error. The current selection's
|
|
125
|
+
* fields are also re-loaded automatically when `selection.projectId`
|
|
126
|
+
* changes. */
|
|
127
|
+
loadProjectFields: (id: string) => Promise<Record<string, any> | null>;
|
|
128
|
+
/** Stash freshly-known project_fields without a round trip — used
|
|
129
|
+
* by the create / edit dialogs after a successful submit. */
|
|
130
|
+
setProjectFields: (id: string, fields: Record<string, any>) => void;
|
|
131
|
+
|
|
93
132
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
94
133
|
* omitted to aggregate runs across every method in the project —
|
|
95
134
|
* the History tab uses this. */
|
|
@@ -120,6 +159,14 @@ const TisContext = createContext<TisContextValue>({
|
|
|
120
159
|
state: EMPTY_STATE,
|
|
121
160
|
selection: EMPTY_SELECTION,
|
|
122
161
|
setSelection: () => {},
|
|
162
|
+
existingProjects: [],
|
|
163
|
+
projectKnown: () => false,
|
|
164
|
+
refreshProjects: async () => {},
|
|
165
|
+
markProjectJustCreated: () => {},
|
|
166
|
+
projectFields: {},
|
|
167
|
+
projectFieldsLoaded: false,
|
|
168
|
+
loadProjectFields: async () => null,
|
|
169
|
+
setProjectFields: () => {},
|
|
123
170
|
fetchRuns: async () => [],
|
|
124
171
|
fetchRun: async () => null,
|
|
125
172
|
runCache: {},
|
|
@@ -340,11 +387,111 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
340
387
|
|
|
341
388
|
const runCache = useMemo(() => ({ ...cacheRef.current }), [cacheVersion]);
|
|
342
389
|
|
|
390
|
+
// -----------------------------------------------------------------
|
|
391
|
+
// Project management state
|
|
392
|
+
//
|
|
393
|
+
// Mirrors what TestSetupForm used to track locally, but lifted up
|
|
394
|
+
// here so <ProjectSelector> on the Project tab and <TestSetupForm>
|
|
395
|
+
// on the Test tab share a single source of truth. Without this
|
|
396
|
+
// lift, the two tabs would each fire their own list_projects and
|
|
397
|
+
// disagree on which IDs are valid.
|
|
398
|
+
// -----------------------------------------------------------------
|
|
399
|
+
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
400
|
+
const justCreatedRef = useRef<Set<string>>(new Set());
|
|
401
|
+
const [projectsTick, setProjectsTick] = useState(0); // bumps on Set mutation
|
|
402
|
+
const [projectFieldsCache, setProjectFieldsCache] = useState<Record<string, Record<string, any>>>({});
|
|
403
|
+
|
|
404
|
+
const refreshProjects = useCallback(async () => {
|
|
405
|
+
try {
|
|
406
|
+
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request, {} as any);
|
|
407
|
+
if (resp?.success && resp.data?.projects) {
|
|
408
|
+
setExistingProjects(resp.data.projects as string[]);
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error('[TisProvider] tis.list_projects failed:', e);
|
|
412
|
+
}
|
|
413
|
+
}, [invoke]);
|
|
414
|
+
|
|
415
|
+
const markProjectJustCreated = useCallback((id: string) => {
|
|
416
|
+
if (!id) return;
|
|
417
|
+
if (justCreatedRef.current.has(id)) return;
|
|
418
|
+
justCreatedRef.current.add(id);
|
|
419
|
+
setProjectsTick(t => t + 1);
|
|
420
|
+
}, []);
|
|
421
|
+
|
|
422
|
+
const projectKnown = useCallback((id: string) => {
|
|
423
|
+
if (!id) return false;
|
|
424
|
+
if (justCreatedRef.current.has(id)) return true;
|
|
425
|
+
return existingProjects.includes(id);
|
|
426
|
+
// existingProjects + projectsTick are deps but useCallback
|
|
427
|
+
// closes over them; consumers read current values fine.
|
|
428
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
429
|
+
}, [existingProjects, projectsTick]);
|
|
430
|
+
|
|
431
|
+
const setProjectFields = useCallback((id: string, fields: Record<string, any>) => {
|
|
432
|
+
if (!id) return;
|
|
433
|
+
setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
|
|
434
|
+
}, []);
|
|
435
|
+
|
|
436
|
+
const loadProjectFields = useCallback(async (id: string): Promise<Record<string, any> | null> => {
|
|
437
|
+
if (!id) return null;
|
|
438
|
+
try {
|
|
439
|
+
const resp: any = await invoke('tis.read_project' as any, MessageType.Request, { project_id: id } as any);
|
|
440
|
+
if (resp?.success) {
|
|
441
|
+
const fields = (resp.data?.project_fields ?? {}) as Record<string, any>;
|
|
442
|
+
setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
|
|
443
|
+
return fields;
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.warn('[TisProvider] tis.read_project failed:', e);
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}, [invoke]);
|
|
450
|
+
|
|
451
|
+
// Initial project list load + refresh on server-side mutation
|
|
452
|
+
// broadcasts. Mark-just-created is purely local; the server
|
|
453
|
+
// broadcasts kick the persisted list back into sync if a project
|
|
454
|
+
// was added by another client (or the next time `acctl` writes a
|
|
455
|
+
// new directory).
|
|
456
|
+
useEffect(() => { void refreshProjects(); }, [refreshProjects]);
|
|
457
|
+
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
const onCreated = () => { void refreshProjects(); };
|
|
460
|
+
const onUpdated = (payload: any) => {
|
|
461
|
+
const pid = typeof payload?.project_id === 'string' ? payload.project_id : '';
|
|
462
|
+
if (pid) void loadProjectFields(pid);
|
|
463
|
+
};
|
|
464
|
+
const id1 = subscribe('tis.project_created', onCreated);
|
|
465
|
+
const id2 = subscribe('tis.project_updated', onUpdated);
|
|
466
|
+
return () => { unsubscribe(id1); unsubscribe(id2); };
|
|
467
|
+
}, [subscribe, unsubscribe, refreshProjects, loadProjectFields]);
|
|
468
|
+
|
|
469
|
+
// Auto-fetch project_fields whenever the selection lands on a
|
|
470
|
+
// known project we don't yet have cached. The Test tab's stage
|
|
471
|
+
// payload depends on this.
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
const pid = selection.projectId;
|
|
474
|
+
if (!pid || !projectKnown(pid)) return;
|
|
475
|
+
if (projectFieldsCache[pid] !== undefined) return;
|
|
476
|
+
void loadProjectFields(pid);
|
|
477
|
+
}, [selection.projectId, projectKnown, projectFieldsCache, loadProjectFields]);
|
|
478
|
+
|
|
479
|
+
const projectFields = projectFieldsCache[selection.projectId] ?? {};
|
|
480
|
+
const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
|
|
481
|
+
|
|
343
482
|
const value: TisContextValue = useMemo(() => ({
|
|
344
483
|
schemas, defaultMethodId, schemasLoaded,
|
|
345
484
|
state, selection, setSelection,
|
|
485
|
+
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
486
|
+
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
487
|
+
fetchRuns, fetchRun, runCache,
|
|
488
|
+
}), [
|
|
489
|
+
schemas, defaultMethodId, schemasLoaded,
|
|
490
|
+
state, selection, setSelection,
|
|
491
|
+
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
492
|
+
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
346
493
|
fetchRuns, fetchRun, runCache,
|
|
347
|
-
|
|
494
|
+
]);
|
|
348
495
|
|
|
349
496
|
return <TisContext.Provider value={value}>{children}</TisContext.Provider>;
|
|
350
497
|
};
|