@adcops/autocore-react 3.3.59 → 3.3.63
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/ams/AmsProvider.d.ts +45 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -0
- package/dist/components/ams/AmsProvider.js +1 -0
- package/dist/components/ams/AssetDetailView.d.ts +3 -0
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -0
- package/dist/components/ams/AssetDetailView.js +1 -0
- package/dist/components/ams/AssetRegistryTable.d.ts +3 -0
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -0
- package/dist/components/ams/AssetRegistryTable.js +1 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts +10 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -0
- package/dist/components/ams/CalibrationEntryDialog.js +1 -0
- package/dist/components/ams/SubLocationPicker.d.ts +3 -0
- package/dist/components/ams/SubLocationPicker.d.ts.map +1 -0
- package/dist/components/ams/SubLocationPicker.js +1 -0
- package/dist/components/ams/index.d.ts +6 -0
- package/dist/components/ams/index.d.ts.map +1 -0
- package/dist/components/ams/index.js +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- 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 +9 -1
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +8 -4
- 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 +45 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/core/AutoCoreTagContext.d.ts +16 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +67 -37
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +1 -1
- package/src/components/ams/AmsProvider.tsx +219 -0
- package/src/components/ams/AssetDetailView.tsx +101 -0
- package/src/components/ams/AssetRegistryTable.tsx +171 -0
- package/src/components/ams/CalibrationEntryDialog.tsx +197 -0
- package/src/components/ams/SubLocationPicker.tsx +146 -0
- package/src/components/ams/index.ts +12 -0
- package/src/components/index.ts +30 -0
- package/src/components/tis/ProjectSelector.tsx +190 -0
- package/src/components/tis/TestDataView.tsx +321 -28
- package/src/components/tis/TestSetupForm.tsx +66 -253
- package/src/components/tis/TisProvider.tsx +192 -1
- package/src/core/AutoCoreTagContext.tsx +114 -16
- package/src/themes/adc-dark/_extensions.scss +15 -0
- package/src/themes/adc-dark/blue/adc_theme.scss +56 -10
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext, useMemo
|
|
2
|
-
import { AutoComplete } from 'primereact/autocomplete';
|
|
3
|
-
import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
4
2
|
import { Button } from 'primereact/button';
|
|
5
3
|
import { InputText } from 'primereact/inputtext';
|
|
6
4
|
import { Tooltip } from 'primereact/tooltip';
|
|
@@ -10,7 +8,6 @@ import { MessageType } from '../../hub/CommandMessage';
|
|
|
10
8
|
import { ValueInput } from '../ValueInput';
|
|
11
9
|
import { TextInput } from '../TextInput';
|
|
12
10
|
import { useTis } from './TisProvider';
|
|
13
|
-
import { ProjectInfoDialog } from './ProjectInfoDialog';
|
|
14
11
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
15
12
|
|
|
16
13
|
export interface TestFieldDef {
|
|
@@ -41,14 +38,18 @@ export interface TestMethod {
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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).
|
|
46
|
+
*
|
|
47
|
+
* All props are optional overrides — by default the form drives
|
|
48
|
+
* itself from the surrounding `<TisProvider>`.
|
|
46
49
|
*/
|
|
47
50
|
export interface TestSetupFormProps {
|
|
48
51
|
schema?: TestMethod;
|
|
49
|
-
defaultProjectId?: string;
|
|
50
52
|
defaultMethodId?: string;
|
|
51
|
-
onProjectChange?: (projectId: string) => void;
|
|
52
53
|
onMethodChange?: (methodId: string) => void;
|
|
53
54
|
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
54
55
|
}
|
|
@@ -65,25 +66,14 @@ const labelOf = (f: TestFieldDef): string => {
|
|
|
65
66
|
const hasDescription = (f: TestFieldDef): boolean =>
|
|
66
67
|
typeof f.description === 'string' && f.description.length > 0;
|
|
67
68
|
|
|
68
|
-
/** Display name for one method: prefer schema's `label`, fall back
|
|
69
|
-
* to the canonical method_id. Mirrors the helper in TestMethodDialog
|
|
70
|
-
* so the row label stays in sync with what the dialog shows. */
|
|
71
69
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
72
70
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
73
71
|
|
|
74
|
-
// Project IDs follow the same character class as the server's
|
|
75
|
-
// `tis.create_project` validator. Keep these in sync — see
|
|
76
|
-
// `src/tis_servelet.rs::create_project`.
|
|
77
|
-
const PROJECT_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
78
|
-
const isValidProjectIdFormat = (id: string) => PROJECT_ID_RE.test(id);
|
|
79
|
-
|
|
80
72
|
// -------------------------------------------------------------------------
|
|
81
73
|
|
|
82
74
|
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
83
75
|
schema: schemaOverride,
|
|
84
|
-
defaultProjectId,
|
|
85
76
|
defaultMethodId,
|
|
86
|
-
onProjectChange,
|
|
87
77
|
onMethodChange,
|
|
88
78
|
onValidationChange,
|
|
89
79
|
}) => {
|
|
@@ -93,9 +83,12 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
93
83
|
|
|
94
84
|
const methodIds = useMemo(() => Object.keys(tis.schemas), [tis.schemas]);
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
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
|
+
|
|
99
92
|
const [methodId, setMethodIdLocal] = useState<string>(
|
|
100
93
|
tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
|
|
101
94
|
);
|
|
@@ -104,14 +97,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
104
97
|
|
|
105
98
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
106
99
|
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (tis.selection.projectId !== projectId) {
|
|
109
|
-
tis.setSelection({ projectId });
|
|
110
|
-
}
|
|
111
|
-
if (onProjectChange) onProjectChange(projectId);
|
|
112
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
-
}, [projectId]);
|
|
114
|
-
|
|
115
100
|
useEffect(() => {
|
|
116
101
|
if (tis.selection.methodId !== methodId && methodId) {
|
|
117
102
|
tis.setSelection({ methodId });
|
|
@@ -134,84 +119,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
134
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
120
|
}, [tis.state.stagedSampleId]);
|
|
136
121
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const [justCreatedTick, setJustCreatedTick] = useState(0);
|
|
141
|
-
const [isValid, setIsValid] = useState(false);
|
|
142
|
-
|
|
143
|
-
// Cache of `project_fields` for the currently-selected project,
|
|
144
|
-
// fetched once on project selection. Folded into every
|
|
145
|
-
// `tis.stage_test` payload so the recorded test.json carries the
|
|
146
|
-
// project-level metadata even though the operator no longer sees
|
|
147
|
-
// those fields in the main form. Keyed by projectId so switching
|
|
148
|
-
// projects mid-session refetches cleanly.
|
|
149
|
-
const [projectFieldsCache, setProjectFieldsCache] = useState<Record<string, any>>({});
|
|
150
|
-
const projectFieldsForCurrent = projectFieldsCache[projectId] ?? {};
|
|
151
|
-
|
|
152
|
-
// Dialog state for create + edit + method-picker.
|
|
153
|
-
const [newProjectOpen, setNewProjectOpen] = useState(false);
|
|
154
|
-
const [editProjectOpen, setEditProjectOpen] = useState(false);
|
|
155
|
-
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
156
|
-
|
|
157
|
-
const fetchProjects = async () => {
|
|
158
|
-
try {
|
|
159
|
-
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
|
|
160
|
-
if (resp.success && resp.data && resp.data.projects) {
|
|
161
|
-
setExistingProjects(resp.data.projects);
|
|
162
|
-
}
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error('Failed to list projects', err);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
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.
|
|
168
125
|
useEffect(() => {
|
|
169
|
-
|
|
126
|
+
if (tis.selection.methodId && tis.selection.methodId !== methodId) {
|
|
127
|
+
setMethodIdLocal(tis.selection.methodId);
|
|
128
|
+
}
|
|
170
129
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
|
-
}, [
|
|
130
|
+
}, [tis.selection.methodId]);
|
|
172
131
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
for (const id of justCreatedRef.current) s.add(id);
|
|
176
|
-
return s;
|
|
177
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
-
}, [existingProjects, justCreatedTick]);
|
|
179
|
-
|
|
180
|
-
const projectExists = projectId.trim() !== '' && knownProjects.has(projectId.trim());
|
|
181
|
-
const projectIdFormatValid = isValidProjectIdFormat(projectId.trim());
|
|
182
|
-
const canCreateProject =
|
|
183
|
-
projectId.trim() !== ''
|
|
184
|
-
&& projectIdFormatValid
|
|
185
|
-
&& !knownProjects.has(projectId.trim());
|
|
186
|
-
|
|
187
|
-
// Whenever the user selects a known project (or the project
|
|
188
|
-
// gets created in-session), pull its persisted project_fields
|
|
189
|
-
// so we can fold them into stage_test. We don't refetch on
|
|
190
|
-
// every keystroke — only when projectId actually lands on an
|
|
191
|
-
// existing project we don't yet have cached.
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
const pid = projectId.trim();
|
|
194
|
-
if (!pid || !projectExists) return;
|
|
195
|
-
if (projectFieldsCache[pid] !== undefined) return; // already cached
|
|
196
|
-
let cancelled = false;
|
|
197
|
-
(async () => {
|
|
198
|
-
try {
|
|
199
|
-
const resp: any = await invoke(
|
|
200
|
-
'tis.read_project' as any, MessageType.Request,
|
|
201
|
-
{ project_id: pid } as any,
|
|
202
|
-
);
|
|
203
|
-
if (cancelled) return;
|
|
204
|
-
if (resp?.success) {
|
|
205
|
-
const pf = (resp.data?.project_fields ?? {}) as Record<string, any>;
|
|
206
|
-
setProjectFieldsCache(prev => ({ ...prev, [pid]: pf }));
|
|
207
|
-
}
|
|
208
|
-
} catch (e) {
|
|
209
|
-
console.warn('[TestSetupForm] read_project failed:', e);
|
|
210
|
-
}
|
|
211
|
-
})();
|
|
212
|
-
return () => { cancelled = true; };
|
|
213
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
-
}, [projectId, projectExists]);
|
|
132
|
+
const [isValid, setIsValid] = useState(false);
|
|
133
|
+
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
215
134
|
|
|
216
135
|
// Seed and live-update config_fields that declare a `source`.
|
|
217
136
|
useEffect(() => {
|
|
@@ -233,22 +152,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
233
152
|
});
|
|
234
153
|
}, [schema, rawValues, findTagByFqdn]);
|
|
235
154
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Validation drives both the local UI and the auto-stage. Project
|
|
242
|
-
// ID must be a known project — typing an unknown name is invalid
|
|
243
|
-
// until the operator goes through the New Project dialog. Each
|
|
244
|
-
// required *config_field* must also be filled in; project_fields
|
|
245
|
-
// are validated by the dialog at create/edit time, not here.
|
|
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.
|
|
246
160
|
useEffect(() => {
|
|
247
161
|
if (!schema) { setIsValid(false); return; }
|
|
248
162
|
let valid = true;
|
|
249
163
|
if (!projectExists) valid = false;
|
|
250
164
|
if (!methodId.trim()) valid = false;
|
|
251
165
|
if (!sampleId.trim()) valid = false;
|
|
166
|
+
if (valid && !tis.projectFieldsLoaded) valid = false;
|
|
252
167
|
|
|
253
168
|
for (const field of schema.config_fields) {
|
|
254
169
|
if (field.name === 'sample_id') continue;
|
|
@@ -258,28 +173,17 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
258
173
|
}
|
|
259
174
|
}
|
|
260
175
|
|
|
261
|
-
// We also gate validity on having loaded the project_fields
|
|
262
|
-
// for the selected project — staging *without* them would
|
|
263
|
-
// record a test.json that's missing project-level metadata
|
|
264
|
-
// for the lifetime of the run. The fetch happens automatically
|
|
265
|
-
// when the project is selected, so this is a tight transient
|
|
266
|
-
// window in practice.
|
|
267
|
-
if (valid && projectExists && projectFieldsCache[projectId.trim()] === undefined) {
|
|
268
|
-
valid = false;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
176
|
setIsValid(valid);
|
|
272
177
|
if (onValidationChange) onValidationChange(valid, config);
|
|
273
178
|
|
|
274
179
|
if (valid) {
|
|
275
180
|
const { sample_id: _drop, ...configRest } = (config ?? {}) as any;
|
|
276
181
|
// Combine persisted project_fields (managerial setup) with
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// run than the project metadata.
|
|
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.
|
|
281
185
|
const mergedConfig = {
|
|
282
|
-
...
|
|
186
|
+
...tis.projectFields,
|
|
283
187
|
...configRest,
|
|
284
188
|
};
|
|
285
189
|
void invoke('tis.stage_test' as any, MessageType.Request, {
|
|
@@ -290,7 +194,11 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
290
194
|
} as any).catch(e => console.error('[TestSetupForm] stage_test failed:', e));
|
|
291
195
|
}
|
|
292
196
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
293
|
-
}, [
|
|
197
|
+
}, [
|
|
198
|
+
config, schema, projectId, methodId, sampleId,
|
|
199
|
+
projectExists, tis.projectFields, tis.projectFieldsLoaded,
|
|
200
|
+
onValidationChange, invoke,
|
|
201
|
+
]);
|
|
294
202
|
|
|
295
203
|
const isFieldValid = (field: TestFieldDef) => {
|
|
296
204
|
if (!field.required) return true;
|
|
@@ -298,11 +206,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
298
206
|
return v !== undefined && v !== '' && v !== null;
|
|
299
207
|
};
|
|
300
208
|
|
|
301
|
-
const handleProjectIdChange = (value: string | null | undefined) => {
|
|
302
|
-
const sanitized = (value || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
303
|
-
setProjectIdLocal(sanitized);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
209
|
const handleSampleIdChange = (value: string) => {
|
|
307
210
|
setSampleIdLocal(value);
|
|
308
211
|
};
|
|
@@ -315,26 +218,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
315
218
|
}
|
|
316
219
|
};
|
|
317
220
|
|
|
318
|
-
// Called by ProjectInfoDialog after a successful create/update.
|
|
319
|
-
// Populates the local cache + known-projects set so the main form
|
|
320
|
-
// is immediately valid without requiring a refresh.
|
|
321
|
-
const handleProjectInfoSubmitted = (pid: string, projectFields: Record<string, any>) => {
|
|
322
|
-
justCreatedRef.current.add(pid);
|
|
323
|
-
setJustCreatedTick(t => t + 1);
|
|
324
|
-
setProjectFieldsCache(prev => ({ ...prev, [pid]: projectFields }));
|
|
325
|
-
// Refresh the dropdown so the new project shows up for any
|
|
326
|
-
// future searches in this session.
|
|
327
|
-
void fetchProjects();
|
|
328
|
-
// If the dialog created a brand-new project, also surface it
|
|
329
|
-
// as the current selection — the operator's intent is clearly
|
|
330
|
-
// "set up this project and start working on it."
|
|
331
|
-
if (projectId.trim() !== pid) setProjectIdLocal(pid);
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
// -----------------------------------------------------------------
|
|
335
|
-
// Per-config-field row renderer — same four-column layout as before:
|
|
336
|
-
// label[units] | input | info-icon | validity-icon
|
|
337
|
-
// -----------------------------------------------------------------
|
|
338
221
|
const renderConfigField = (field: TestFieldDef) => {
|
|
339
222
|
if (field.name === 'sample_id') return null;
|
|
340
223
|
const valid = isFieldValid(field);
|
|
@@ -389,81 +272,46 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
389
272
|
);
|
|
390
273
|
}
|
|
391
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
|
+
|
|
392
293
|
const gridStyle: React.CSSProperties = {
|
|
393
294
|
padding: '1.25rem',
|
|
394
295
|
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
395
296
|
};
|
|
396
297
|
|
|
397
|
-
const projectRowValid = projectExists;
|
|
398
|
-
|
|
399
298
|
return (
|
|
400
299
|
<div className="ac-form-grid" style={gridStyle}>
|
|
401
300
|
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
402
|
-
|
|
301
|
+
Test Setup
|
|
403
302
|
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
404
303
|
<i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
|
|
405
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>
|
|
406
313
|
</h3>
|
|
407
314
|
|
|
408
|
-
<span className="ac-form-label">Project ID</span>
|
|
409
|
-
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
410
|
-
<AutoComplete
|
|
411
|
-
value={projectId}
|
|
412
|
-
suggestions={filteredProjects}
|
|
413
|
-
completeMethod={searchProjects}
|
|
414
|
-
onChange={(e) => handleProjectIdChange(e.value)}
|
|
415
|
-
dropdown
|
|
416
|
-
placeholder="Select an existing Project ID, or type a new one and click +"
|
|
417
|
-
className={!projectRowValid ? 'p-invalid' : ''}
|
|
418
|
-
style={{ flex: 1 }}
|
|
419
|
-
/>
|
|
420
|
-
{/*
|
|
421
|
-
* + button → opens the New Project dialog where the
|
|
422
|
-
* operator (or manager) fills in project_fields.
|
|
423
|
-
* Enabled only when the typed ID is a fresh, valid
|
|
424
|
-
* candidate. Once the dialog completes, the project
|
|
425
|
-
* is created on the server and added to our local
|
|
426
|
-
* known-projects set so the main form becomes valid
|
|
427
|
-
* immediately.
|
|
428
|
-
*/}
|
|
429
|
-
<Button
|
|
430
|
-
icon="pi pi-plus"
|
|
431
|
-
type="button"
|
|
432
|
-
onClick={() => setNewProjectOpen(true)}
|
|
433
|
-
disabled={!canCreateProject}
|
|
434
|
-
tooltip={
|
|
435
|
-
!projectId.trim() ? 'Type a project ID first' :
|
|
436
|
-
!projectIdFormatValid ? 'Letters, digits, _ and - only' :
|
|
437
|
-
knownProjects.has(projectId.trim()) ? 'Project already exists' :
|
|
438
|
-
`Create project "${projectId.trim()}"`
|
|
439
|
-
}
|
|
440
|
-
tooltipOptions={{ position: 'top' }}
|
|
441
|
-
/>
|
|
442
|
-
{/*
|
|
443
|
-
* ✏️ button → opens the Edit Project Information
|
|
444
|
-
* dialog. Enabled only when the selected project
|
|
445
|
-
* actually exists. This is the only way to mutate
|
|
446
|
-
* project_fields after creation; the operator can't
|
|
447
|
-
* stumble into editing project metadata while running
|
|
448
|
-
* a sample, which keeps the future per-user permission
|
|
449
|
-
* gate (manager vs operator) clean.
|
|
450
|
-
*/}
|
|
451
|
-
<Button
|
|
452
|
-
icon="pi pi-pencil"
|
|
453
|
-
type="button"
|
|
454
|
-
onClick={() => setEditProjectOpen(true)}
|
|
455
|
-
disabled={!projectExists}
|
|
456
|
-
tooltip={projectExists
|
|
457
|
-
? `Edit information for "${projectId.trim()}"`
|
|
458
|
-
: 'Select an existing project to edit'}
|
|
459
|
-
tooltipOptions={{ position: 'top' }}
|
|
460
|
-
/>
|
|
461
|
-
</div>
|
|
462
|
-
<span aria-hidden="true" />
|
|
463
|
-
<span style={{ color: projectRowValid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
464
|
-
<i className={projectRowValid ? 'pi pi-check' : 'pi pi-times'} />
|
|
465
|
-
</span>
|
|
466
|
-
|
|
467
315
|
<span className="ac-form-label">Sample ID</span>
|
|
468
316
|
<TextInput
|
|
469
317
|
label={undefined}
|
|
@@ -476,18 +324,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
476
324
|
<i className={sampleId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
477
325
|
</span>
|
|
478
326
|
|
|
479
|
-
{/*
|
|
480
|
-
* Test Method row. Shows the current method's pretty
|
|
481
|
-
* label (or its canonical method_id when no label is
|
|
482
|
-
* declared) read-only, with an edit button that opens
|
|
483
|
-
* the picker dialog. The dialog scales past three or
|
|
484
|
-
* four methods where a SelectButton would wrap, and
|
|
485
|
-
* surfaces the per-method description so the operator
|
|
486
|
-
* can disambiguate similarly-named methods at the
|
|
487
|
-
* point of choice. The row is rendered even when only
|
|
488
|
-
* one method is declared so the operator can still open
|
|
489
|
-
* the picker and read its description.
|
|
490
|
-
*/}
|
|
491
327
|
{methodIds.length > 0 && (
|
|
492
328
|
<>
|
|
493
329
|
<span className="ac-form-label">Test Method</span>
|
|
@@ -518,29 +354,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
518
354
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
519
355
|
{schema.config_fields.map(renderConfigField)}
|
|
520
356
|
|
|
521
|
-
{/*
|
|
522
|
-
* Project Information no longer renders inline. Use the
|
|
523
|
-
* + and ✏️ buttons in the Project ID row above to create
|
|
524
|
-
* or edit it. The dialogs persist project_fields on the
|
|
525
|
-
* server and the form folds them into stage_test
|
|
526
|
-
* automatically.
|
|
527
|
-
*/}
|
|
528
|
-
<ProjectInfoDialog
|
|
529
|
-
visible={newProjectOpen}
|
|
530
|
-
onHide={() => setNewProjectOpen(false)}
|
|
531
|
-
mode="create"
|
|
532
|
-
projectId={projectId.trim()}
|
|
533
|
-
projectFields={schema.project_fields}
|
|
534
|
-
onSubmitted={handleProjectInfoSubmitted}
|
|
535
|
-
/>
|
|
536
|
-
<ProjectInfoDialog
|
|
537
|
-
visible={editProjectOpen}
|
|
538
|
-
onHide={() => setEditProjectOpen(false)}
|
|
539
|
-
mode="edit"
|
|
540
|
-
projectId={projectId.trim()}
|
|
541
|
-
projectFields={schema.project_fields}
|
|
542
|
-
onSubmitted={handleProjectInfoSubmitted}
|
|
543
|
-
/>
|
|
544
357
|
<TestMethodDialog
|
|
545
358
|
visible={methodPickerOpen}
|
|
546
359
|
onHide={() => setMethodPickerOpen(false)}
|