@adcops/autocore-react 3.3.48 → 3.3.54

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 (38) hide show
  1. package/dist/components/index.d.ts +10 -2
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis/ResultHistoryTable.d.ts +19 -0
  5. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -0
  6. package/dist/components/tis/ResultHistoryTable.js +1 -0
  7. package/dist/components/{TestDataView.d.ts → tis/TestDataView.d.ts} +9 -5
  8. package/dist/components/tis/TestDataView.d.ts.map +1 -0
  9. package/dist/components/tis/TestDataView.js +1 -0
  10. package/dist/components/tis/TestRawDataView.d.ts +18 -0
  11. package/dist/components/tis/TestRawDataView.d.ts.map +1 -0
  12. package/dist/components/tis/TestRawDataView.js +1 -0
  13. package/dist/components/tis/TestSetupForm.d.ts +35 -0
  14. package/dist/components/tis/TestSetupForm.d.ts.map +1 -0
  15. package/dist/components/tis/TestSetupForm.js +1 -0
  16. package/dist/components/tis/TisProvider.d.ts +93 -0
  17. package/dist/components/tis/TisProvider.d.ts.map +1 -0
  18. package/dist/components/tis/TisProvider.js +1 -0
  19. package/package.json +1 -1
  20. package/src/components/index.ts +40 -2
  21. package/src/components/tis/ResultHistoryTable.tsx +277 -0
  22. package/src/components/{TestDataView.tsx → tis/TestDataView.tsx} +72 -38
  23. package/src/components/{TestRawDataView.tsx → tis/TestRawDataView.tsx} +41 -24
  24. package/src/components/tis/TestSetupForm.tsx +314 -0
  25. package/src/components/tis/TisProvider.tsx +404 -0
  26. package/dist/components/ResultHistoryTable.d.ts +0 -7
  27. package/dist/components/ResultHistoryTable.d.ts.map +0 -1
  28. package/dist/components/ResultHistoryTable.js +0 -1
  29. package/dist/components/TestDataView.d.ts.map +0 -1
  30. package/dist/components/TestDataView.js +0 -1
  31. package/dist/components/TestRawDataView.d.ts +0 -14
  32. package/dist/components/TestRawDataView.d.ts.map +0 -1
  33. package/dist/components/TestRawDataView.js +0 -1
  34. package/dist/components/TestSetupForm.d.ts +0 -24
  35. package/dist/components/TestSetupForm.d.ts.map +0 -1
  36. package/dist/components/TestSetupForm.js +0 -1
  37. package/src/components/ResultHistoryTable.tsx +0 -162
  38. package/src/components/TestSetupForm.tsx +0 -255
@@ -0,0 +1,314 @@
1
+ import React, { useState, useEffect, useContext, useMemo } from 'react';
2
+ import { AutoComplete } from 'primereact/autocomplete';
3
+ import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
4
+ import { SelectButton } from 'primereact/selectbutton';
5
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
6
+ import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
7
+ import { MessageType } from '../../hub/CommandMessage';
8
+ import { ValueInput } from '../ValueInput';
9
+ import { TextInput } from '../TextInput';
10
+ import { useTis } from './TisProvider';
11
+
12
+ export interface TestFieldDef {
13
+ name: string;
14
+ type: string;
15
+ units?: string;
16
+ required?: boolean;
17
+ source?: string;
18
+ }
19
+
20
+ export interface TestMethod {
21
+ project_fields: TestFieldDef[];
22
+ config_fields: TestFieldDef[];
23
+ cycle_fields: TestFieldDef[];
24
+ results_fields: TestFieldDef[];
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
+ */
38
+ export interface TestSetupFormProps {
39
+ schema?: TestMethod;
40
+ defaultProjectId?: string;
41
+ defaultMethodId?: string;
42
+ onProjectChange?: (projectId: string) => void;
43
+ onMethodChange?: (methodId: string) => void;
44
+ onValidationChange?: (isValid: boolean, config: any) => void;
45
+ }
46
+
47
+ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
48
+ schema: schemaOverride,
49
+ defaultProjectId,
50
+ defaultMethodId,
51
+ onProjectChange,
52
+ onMethodChange,
53
+ onValidationChange,
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>('');
71
+ const [config, setConfig] = useState<any>({});
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.
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
+ useEffect(() => {
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
+
114
+ const [existingProjects, setExistingProjects] = useState<string[]>([]);
115
+ const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
116
+ const [isValid, setIsValid] = useState(false);
117
+
118
+ // Seed and live-update fields that declare a `source` (FQDN).
119
+ useEffect(() => {
120
+ if (!schema) return;
121
+ const allFields = [...schema.project_fields, ...schema.config_fields];
122
+ setConfig((prev: any) => {
123
+ let next = prev;
124
+ for (const field of allFields) {
125
+ if (field.name === 'sample_id') continue; // sample_id is top-level now
126
+ if (!field.source) continue;
127
+ const tag = findTagByFqdn(field.source);
128
+ if (!tag) continue;
129
+ const rawVal = rawValues[tag.tagName];
130
+ if (rawVal === undefined || rawVal === null) continue;
131
+ if (next[field.name] === rawVal) continue;
132
+ if (next === prev) next = { ...prev };
133
+ next[field.name] = rawVal;
134
+ }
135
+ return next;
136
+ });
137
+ }, [schema, rawValues, findTagByFqdn]);
138
+
139
+ useEffect(() => {
140
+ const fetchProjects = async () => {
141
+ try {
142
+ const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
143
+ if (resp.success && resp.data && resp.data.projects) {
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.
162
+ useEffect(() => {
163
+ if (!schema) { setIsValid(false); return; }
164
+ let valid = true;
165
+ if (!projectId.trim()) valid = false;
166
+ if (!methodId.trim()) valid = false;
167
+ if (!sampleId.trim()) valid = false;
168
+
169
+ const allFields = [...schema.project_fields, ...schema.config_fields];
170
+ for (const field of allFields) {
171
+ if (field.name === 'sample_id') continue; // tracked separately
172
+ if (field.required) {
173
+ const v = config[field.name];
174
+ if (v === undefined || v === '' || v === null) { valid = false; break; }
175
+ }
176
+ }
177
+ setIsValid(valid);
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));
192
+ }
193
+ }, [config, schema, projectId, methodId, sampleId, onValidationChange, invoke]);
194
+
195
+ const isFieldValid = (field: TestFieldDef) => {
196
+ if (!field.required) return true;
197
+ const v = config[field.name];
198
+ return v !== undefined && v !== '' && v !== null;
199
+ };
200
+
201
+ const handleProjectIdChange = (value: string | null | undefined) => {
202
+ const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
203
+ setProjectIdLocal(sanitized);
204
+ };
205
+
206
+ const handleSampleIdChange = (value: string) => {
207
+ setSampleIdLocal(value);
208
+ };
209
+
210
+ const handleFieldChange = async (field: TestFieldDef, val: any) => {
211
+ setConfig({ ...config, [field.name]: val });
212
+ if (field.source) {
213
+ try { await write(field.source, val); }
214
+ catch (e) { console.error('Failed to write to source:', e); }
215
+ }
216
+ };
217
+
218
+ const renderField = (field: TestFieldDef) => {
219
+ if (field.name === 'sample_id') return null;
220
+ const valid = isFieldValid(field);
221
+ const isNum = field.type !== 'string' && field.type !== 'bool';
222
+ return (
223
+ <React.Fragment key={field.name}>
224
+ <span className="ac-form-label">{field.name}</span>
225
+ {isNum ? (
226
+ <ValueInput
227
+ label={undefined}
228
+ value={config[field.name] != null ? Number(config[field.name]) : null}
229
+ onValueChanged={(val) => handleFieldChange(field, val)}
230
+ className={!valid ? 'p-invalid' : ''}
231
+ />
232
+ ) : (
233
+ <TextInput
234
+ label={undefined}
235
+ value={config[field.name] != null ? String(config[field.name]) : ''}
236
+ onValueChanged={(val) => handleFieldChange(field, val)}
237
+ className={!valid ? 'p-invalid' : ''}
238
+ />
239
+ )}
240
+ <span className="ac-form-units">{field.units ?? ''}</span>
241
+ <span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
242
+ <i className={valid ? 'pi pi-check' : 'pi pi-times'} />
243
+ </span>
244
+ </React.Fragment>
245
+ );
246
+ };
247
+
248
+ if (!schema) {
249
+ return (
250
+ <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
251
+ <h3 className="ac-form-section">
252
+ {tis.schemasLoaded ? 'No Test Method Selected' : 'Loading test methods…'}
253
+ </h3>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ return (
259
+ <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
260
+ <h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
261
+ Project &amp; Method
262
+ <span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
263
+ <i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
264
+ </span>
265
+ </h3>
266
+
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
+ <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' : ''}
288
+ />
289
+ <span />
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'} />
292
+ </span>
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
+
307
+ <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
308
+ {schema.project_fields.map(renderField)}
309
+
310
+ <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
311
+ {schema.config_fields.map(renderField)}
312
+ </div>
313
+ );
314
+ };