@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.
Files changed (53) hide show
  1. package/dist/components/ams/AmsProvider.d.ts +45 -0
  2. package/dist/components/ams/AmsProvider.d.ts.map +1 -0
  3. package/dist/components/ams/AmsProvider.js +1 -0
  4. package/dist/components/ams/AssetDetailView.d.ts +3 -0
  5. package/dist/components/ams/AssetDetailView.d.ts.map +1 -0
  6. package/dist/components/ams/AssetDetailView.js +1 -0
  7. package/dist/components/ams/AssetRegistryTable.d.ts +3 -0
  8. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -0
  9. package/dist/components/ams/AssetRegistryTable.js +1 -0
  10. package/dist/components/ams/CalibrationEntryDialog.d.ts +10 -0
  11. package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -0
  12. package/dist/components/ams/CalibrationEntryDialog.js +1 -0
  13. package/dist/components/ams/SubLocationPicker.d.ts +3 -0
  14. package/dist/components/ams/SubLocationPicker.d.ts.map +1 -0
  15. package/dist/components/ams/SubLocationPicker.js +1 -0
  16. package/dist/components/ams/index.d.ts +6 -0
  17. package/dist/components/ams/index.d.ts.map +1 -0
  18. package/dist/components/ams/index.js +1 -0
  19. package/dist/components/index.d.ts +9 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js +1 -1
  22. package/dist/components/tis/ProjectSelector.d.ts +15 -0
  23. package/dist/components/tis/ProjectSelector.d.ts.map +1 -0
  24. package/dist/components/tis/ProjectSelector.js +1 -0
  25. package/dist/components/tis/TestDataView.d.ts +9 -1
  26. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  27. package/dist/components/tis/TestDataView.js +1 -1
  28. package/dist/components/tis/TestSetupForm.d.ts +8 -4
  29. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  30. package/dist/components/tis/TestSetupForm.js +1 -1
  31. package/dist/components/tis/TisProvider.d.ts +45 -0
  32. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  33. package/dist/components/tis/TisProvider.js +1 -1
  34. package/dist/core/AutoCoreTagContext.d.ts +16 -0
  35. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  36. package/dist/core/AutoCoreTagContext.js +1 -1
  37. package/dist/themes/adc-dark/blue/theme.css +67 -37
  38. package/dist/themes/adc-dark/blue/theme.css.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/ams/AmsProvider.tsx +219 -0
  41. package/src/components/ams/AssetDetailView.tsx +101 -0
  42. package/src/components/ams/AssetRegistryTable.tsx +171 -0
  43. package/src/components/ams/CalibrationEntryDialog.tsx +197 -0
  44. package/src/components/ams/SubLocationPicker.tsx +146 -0
  45. package/src/components/ams/index.ts +12 -0
  46. package/src/components/index.ts +30 -0
  47. package/src/components/tis/ProjectSelector.tsx +190 -0
  48. package/src/components/tis/TestDataView.tsx +321 -28
  49. package/src/components/tis/TestSetupForm.tsx +66 -253
  50. package/src/components/tis/TisProvider.tsx +192 -1
  51. package/src/core/AutoCoreTagContext.tsx +114 -16
  52. package/src/themes/adc-dark/_extensions.scss +15 -0
  53. package/src/themes/adc-dark/blue/adc_theme.scss +56 -10
@@ -1,6 +1,4 @@
1
- import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
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
- * Props are all optional overrides by default the form drives itself
45
- * from the surrounding `<TisProvider>`.
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
- const [projectId, setProjectIdLocal] = useState<string>(
97
- tis.selection.projectId || defaultProjectId || ''
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
- const [existingProjects, setExistingProjects] = useState<string[]>([]);
138
- const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
139
- const justCreatedRef = useRef<Set<string>>(new Set());
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
- fetchProjects();
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
- }, [invoke]);
130
+ }, [tis.selection.methodId]);
172
131
 
173
- const knownProjects = useMemo(() => {
174
- const s = new Set<string>(existingProjects);
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
- const searchProjects = (event: AutoCompleteCompleteEvent) => {
237
- const query = event.query.toLowerCase();
238
- setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
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
- // the per-test config_fields the operator just filled in.
278
- // Keys collide in pathological project.json, in which case
279
- // the per-test value wins — operators are closer to the
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
- ...projectFieldsForCurrent,
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
- }, [config, schema, projectId, methodId, sampleId, projectExists, projectFieldsCache, onValidationChange, invoke]);
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
- Project &amp; Method
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)}