@adcops/autocore-react 3.3.50 → 3.3.57

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.
@@ -1,12 +1,13 @@
1
- import React, { useState, useEffect, useContext } from 'react';
1
+ import React, { useState, useEffect, useContext, useMemo } from 'react';
2
2
  import { AutoComplete } from 'primereact/autocomplete';
3
3
  import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
4
+ import { SelectButton } from 'primereact/selectbutton';
4
5
  import { EventEmitterContext } from '../../core/EventEmitterContext';
5
6
  import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
6
7
  import { MessageType } from '../../hub/CommandMessage';
7
8
  import { ValueInput } from '../ValueInput';
8
9
  import { TextInput } from '../TextInput';
9
- import { InputText } from 'primereact/inputtext';
10
+ import { useTis } from './TisProvider';
10
11
 
11
12
  export interface TestFieldDef {
12
13
  name: string;
@@ -16,84 +17,118 @@ export interface TestFieldDef {
16
17
  source?: string;
17
18
  }
18
19
 
19
- export interface TestDefinition {
20
+ export interface TestMethod {
20
21
  project_fields: TestFieldDef[];
21
22
  config_fields: TestFieldDef[];
22
23
  cycle_fields: TestFieldDef[];
23
24
  results_fields: TestFieldDef[];
24
25
  }
25
26
 
27
+ /**
28
+ * Props are all optional overrides — by default the form drives itself
29
+ * from the surrounding `<TisProvider>`. Pass any of these to lock that
30
+ * particular axis.
31
+ *
32
+ * - `schema`: bypass `useTisSchemas()` for the selected method.
33
+ * - `defaultProjectId` / `defaultMethodId`: seed the form's local
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.
37
+ */
26
38
  export interface TestSetupFormProps {
27
- schema: TestDefinition;
39
+ schema?: TestMethod;
28
40
  defaultProjectId?: string;
29
- defaultDefinitionId?: string;
41
+ defaultMethodId?: string;
30
42
  onProjectChange?: (projectId: string) => void;
31
- onDefinitionChange?: (definitionId: string) => void;
43
+ onMethodChange?: (methodId: string) => void;
32
44
  onValidationChange?: (isValid: boolean, config: any) => void;
33
45
  }
34
46
 
35
- export const TestSetupForm: React.FC<TestSetupFormProps> = ({
36
- schema,
37
- defaultProjectId = "",
38
- defaultDefinitionId = "default",
47
+ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
48
+ schema: schemaOverride,
49
+ defaultProjectId,
50
+ defaultMethodId,
39
51
  onProjectChange,
40
- onDefinitionChange,
41
- onValidationChange
52
+ onMethodChange,
53
+ onValidationChange,
42
54
  }) => {
55
+ const tis = useTis();
56
+ const { invoke, write } = useContext(EventEmitterContext);
57
+ const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
58
+
59
+ const methodIds = useMemo(() => Object.keys(tis.schemas), [tis.schemas]);
60
+
61
+ // Seed the form's local project/method state from the provider's
62
+ // selection. Direct edits update the provider via setSelection so
63
+ // the History tab and Data tab follow along.
64
+ const [projectId, setProjectIdLocal] = useState<string>(
65
+ tis.selection.projectId || defaultProjectId || ''
66
+ );
67
+ const [methodId, setMethodIdLocal] = useState<string>(
68
+ tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
69
+ );
70
+ const [sampleId, setSampleIdLocal] = useState<string>('');
43
71
  const [config, setConfig] = useState<any>({});
44
- const [projectId, setProjectId] = useState<string>(defaultProjectId);
45
- const [definitionId, setDefinitionId] = useState<string>(defaultDefinitionId);
46
-
47
- // Notify parent when projectId or definitionId changes
72
+
73
+ // Resolve the schema for the active method. The override beats the
74
+ // registry; if neither is available, render the empty state.
75
+ const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
76
+
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.
48
80
  useEffect(() => {
81
+ if (tis.selection.projectId !== projectId) {
82
+ tis.setSelection({ projectId });
83
+ }
49
84
  if (onProjectChange) onProjectChange(projectId);
50
- }, [projectId, onProjectChange]);
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ }, [projectId]);
51
87
 
52
88
  useEffect(() => {
53
- if (onDefinitionChange) onDefinitionChange(definitionId);
54
- }, [definitionId, onDefinitionChange]);
55
-
89
+ if (tis.selection.methodId !== methodId && methodId) {
90
+ tis.setSelection({ methodId });
91
+ }
92
+ if (onMethodChange) onMethodChange(methodId);
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ }, [methodId]);
95
+
96
+ useEffect(() => {
97
+ if (tis.selection.sampleId !== sampleId) {
98
+ tis.setSelection({ sampleId });
99
+ }
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, [sampleId]);
102
+
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
+ useEffect(() => {
108
+ if (tis.state.stagedSampleId && tis.state.stagedSampleId !== sampleId) {
109
+ setSampleIdLocal(tis.state.stagedSampleId);
110
+ }
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ }, [tis.state.stagedSampleId]);
113
+
56
114
  const [existingProjects, setExistingProjects] = useState<string[]>([]);
57
115
  const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
58
-
59
116
  const [isValid, setIsValid] = useState(false);
60
- const { invoke, write } = useContext(EventEmitterContext);
61
- const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
62
117
 
63
- // Seed and live-update fields that declare a `source`.
64
- //
65
- // The schema's `source` is the remote FQDN (e.g. "gm.x_cycle_move_speed"),
66
- // but the event bus dispatches on the local `tagName` that the
67
- // AutoCoreTagProvider registered. We resolve FQDN → tagName via
68
- // `findTagByFqdn` and then read straight from the provider's `rawValues`,
69
- // which the provider already eager-reads on mount and keeps current via
70
- // subscriptions. That single source covers both the initial population
71
- // (page reload / definition change) and live updates — no separate
72
- // subscribe or invoke needed here.
118
+ // Seed and live-update fields that declare a `source` (FQDN).
73
119
  useEffect(() => {
74
120
  if (!schema) return;
75
-
76
121
  const allFields = [...schema.project_fields, ...schema.config_fields];
77
-
78
122
  setConfig((prev: any) => {
79
123
  let next = prev;
80
124
  for (const field of allFields) {
125
+ if (field.name === 'sample_id') continue; // sample_id is top-level now
81
126
  if (!field.source) continue;
82
-
83
127
  const tag = findTagByFqdn(field.source);
84
- if (!tag) {
85
- console.warn(
86
- `TestSetupForm: no tag registered for source "${field.source}" ` +
87
- `(field "${field.name}"). Add it to acTagSpec with "ux": true ` +
88
- `in project.json, or this field won't populate or update.`
89
- );
90
- continue;
91
- }
92
-
128
+ if (!tag) continue;
93
129
  const rawVal = rawValues[tag.tagName];
94
130
  if (rawVal === undefined || rawVal === null) continue;
95
131
  if (next[field.name] === rawVal) continue;
96
-
97
132
  if (next === prev) next = { ...prev };
98
133
  next[field.name] = rawVal;
99
134
  }
@@ -104,12 +139,12 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
104
139
  useEffect(() => {
105
140
  const fetchProjects = async () => {
106
141
  try {
107
- const resp: any = await invoke('results.list_projects' as any, MessageType.Request as any, {} as any);
142
+ const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
108
143
  if (resp.success && resp.data && resp.data.projects) {
109
144
  setExistingProjects(resp.data.projects);
110
145
  }
111
146
  } catch (err) {
112
- console.error("Failed to list projects", err);
147
+ console.error('Failed to list projects', err);
113
148
  }
114
149
  };
115
150
  fetchProjects();
@@ -120,66 +155,77 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
120
155
  setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
121
156
  };
122
157
 
123
- const handleDefinitionIdChange = (value: string) => {
124
- const sanitized = value.replace(/[^a-zA-Z0-9_]/g, '');
125
- setDefinitionId(sanitized);
126
- };
127
-
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.
128
162
  useEffect(() => {
129
- if (!schema) return;
163
+ if (!schema) { setIsValid(false); return; }
130
164
  let valid = true;
131
- if (!projectId || projectId.trim() === '') valid = false;
132
- if (!definitionId || definitionId.trim() === '') valid = false;
133
-
165
+ if (!projectId.trim()) valid = false;
166
+ if (!methodId.trim()) valid = false;
167
+ if (!sampleId.trim()) valid = false;
168
+
134
169
  const allFields = [...schema.project_fields, ...schema.config_fields];
135
-
136
170
  for (const field of allFields) {
171
+ if (field.name === 'sample_id') continue; // tracked separately
137
172
  if (field.required) {
138
- if (config[field.name] === undefined || config[field.name] === '' || config[field.name] === null) {
139
- valid = false;
140
- break;
141
- }
173
+ const v = config[field.name];
174
+ if (v === undefined || v === '' || v === null) { valid = false; break; }
142
175
  }
143
176
  }
144
177
  setIsValid(valid);
145
- if (onValidationChange) {
146
- onValidationChange(valid, config);
178
+ if (onValidationChange) onValidationChange(valid, config);
179
+
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
+ if (valid) {
185
+ const { sample_id: _drop, ...rest } = (config ?? {}) as any;
186
+ void invoke('tis.stage_test' as any, MessageType.Request, {
187
+ project_id: projectId,
188
+ method_id: methodId,
189
+ sample_id: sampleId,
190
+ config: rest,
191
+ } as any).catch(e => console.error('[TestSetupForm] stage_test failed:', e));
147
192
  }
148
- }, [config, schema, projectId, definitionId, onValidationChange]);
193
+ }, [config, schema, projectId, methodId, sampleId, onValidationChange, invoke]);
149
194
 
150
195
  const isFieldValid = (field: TestFieldDef) => {
151
196
  if (!field.required) return true;
152
- return config[field.name] !== undefined && config[field.name] !== '' && config[field.name] !== null;
197
+ const v = config[field.name];
198
+ return v !== undefined && v !== '' && v !== null;
153
199
  };
154
200
 
155
201
  const handleProjectIdChange = (value: string | null | undefined) => {
156
- const val = value || '';
157
- const sanitized = val.replace(/[^a-zA-Z0-9_]/g, '');
158
- setProjectId(sanitized);
202
+ const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
203
+ setProjectIdLocal(sanitized);
204
+ };
205
+
206
+ const handleSampleIdChange = (value: string) => {
207
+ setSampleIdLocal(value);
159
208
  };
160
209
 
161
210
  const handleFieldChange = async (field: TestFieldDef, val: any) => {
162
- setConfig({...config, [field.name]: val});
211
+ setConfig({ ...config, [field.name]: val });
163
212
  if (field.source) {
164
- try {
165
- await write(field.source, val);
166
- } catch(e) {
167
- console.error("Failed to write to source:", e);
168
- }
213
+ try { await write(field.source, val); }
214
+ catch (e) { console.error('Failed to write to source:', e); }
169
215
  }
170
216
  };
171
217
 
172
218
  const renderField = (field: TestFieldDef) => {
219
+ if (field.name === 'sample_id') return null;
173
220
  const valid = isFieldValid(field);
174
221
  const isNum = field.type !== 'string' && field.type !== 'bool';
175
-
176
222
  return (
177
223
  <React.Fragment key={field.name}>
178
224
  <span className="ac-form-label">{field.name}</span>
179
225
  {isNum ? (
180
226
  <ValueInput
181
227
  label={undefined}
182
- value={config[field.name] != null ? Number(config[field.name]) : null}
228
+ value={config[field.name] != null ? Number(config[field.name]) : null}
183
229
  onValueChanged={(val) => handleFieldChange(field, val)}
184
230
  className={!valid ? 'p-invalid' : ''}
185
231
  />
@@ -191,11 +237,9 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
191
237
  className={!valid ? 'p-invalid' : ''}
192
238
  />
193
239
  )}
194
-
195
240
  <span className="ac-form-units">{field.units ?? ''}</span>
196
-
197
241
  <span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
198
- <i className={valid ? "pi pi-check" : "pi pi-times"}></i>
242
+ <i className={valid ? 'pi pi-check' : 'pi pi-times'} />
199
243
  </span>
200
244
  </React.Fragment>
201
245
  );
@@ -204,7 +248,9 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
204
248
  if (!schema) {
205
249
  return (
206
250
  <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
207
- <h3 className="ac-form-section">No Test Definition Selected</h3>
251
+ <h3 className="ac-form-section">
252
+ {tis.schemasLoaded ? 'No Test Method Selected' : 'Loading test methods…'}
253
+ </h3>
208
254
  </div>
209
255
  );
210
256
  }
@@ -212,12 +258,12 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
212
258
  return (
213
259
  <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
214
260
  <h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
215
- Project & Definition
261
+ Project &amp; Method
216
262
  <span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
217
- <i className={isValid ? "pi pi-check-circle" : "pi pi-exclamation-circle"}></i>
263
+ <i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
218
264
  </span>
219
265
  </h3>
220
-
266
+
221
267
  <span className="ac-form-label">Project ID</span>
222
268
  <AutoComplete
223
269
  value={projectId}
@@ -226,28 +272,41 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
226
272
  onChange={(e) => handleProjectIdChange(e.value)}
227
273
  dropdown
228
274
  placeholder="Enter or select Project ID"
229
- className={!projectId || projectId.trim() === '' ? 'p-invalid' : ''}
275
+ className={!projectId.trim() ? 'p-invalid' : ''}
230
276
  />
231
277
  <span />
232
- <span style={{ color: projectId && projectId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
233
- <i className={projectId && projectId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
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'} />
234
280
  </span>
235
281
 
236
- <span className="ac-form-label">Definition ID</span>
237
- <InputText
238
- value={definitionId}
239
- onChange={(e) => handleDefinitionIdChange(e.target.value)}
240
- placeholder="Enter Definition ID"
241
- className={!definitionId || definitionId.trim() === '' ? 'p-invalid' : ''}
282
+ <span className="ac-form-label">Sample ID</span>
283
+ <TextInput
284
+ label={undefined}
285
+ value={sampleId}
286
+ onValueChanged={handleSampleIdChange}
287
+ className={!sampleId.trim() ? 'p-invalid' : ''}
242
288
  />
243
289
  <span />
244
- <span style={{ color: definitionId && definitionId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
245
- <i className={definitionId && definitionId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
290
+ <span style={{ color: sampleId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
291
+ <i className={sampleId.trim() ? 'pi pi-check' : 'pi pi-times'} />
246
292
  </span>
247
293
 
294
+ {methodIds.length > 1 && (
295
+ <>
296
+ <span className="ac-form-label">Test Method</span>
297
+ <SelectButton
298
+ value={methodId}
299
+ options={methodIds.map(id => ({ label: id, value: id }))}
300
+ onChange={(e) => e.value && setMethodIdLocal(e.value)}
301
+ />
302
+ <span />
303
+ <span />
304
+ </>
305
+ )}
306
+
248
307
  <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
249
308
  {schema.project_fields.map(renderField)}
250
-
309
+
251
310
  <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
252
311
  {schema.config_fields.map(renderField)}
253
312
  </div>