@adcops/autocore-react 3.3.67 → 3.3.68
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/tis/TestSetupForm.d.ts +32 -0
- 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 +15 -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/tis/TestSetupForm.tsx +126 -4
- package/src/components/tis/TisProvider.tsx +34 -0
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One asset_ref declared on a test method. We only consume the subset
|
|
4
|
+
* the form cares about — `field`, `asset_type`, `select`, `from`,
|
|
5
|
+
* `location`. Other fields (calibration_required, label, description)
|
|
6
|
+
* exist on the wire but aren't form-relevant.
|
|
7
|
+
*/
|
|
8
|
+
interface TisAssetRef {
|
|
9
|
+
field: string;
|
|
10
|
+
asset_type: string;
|
|
11
|
+
select: 'by_location' | 'by_id_field';
|
|
12
|
+
from?: string;
|
|
13
|
+
location?: string;
|
|
14
|
+
}
|
|
2
15
|
export interface TestFieldDef {
|
|
3
16
|
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
4
17
|
name: string;
|
|
@@ -17,12 +30,30 @@ export interface TestMethod {
|
|
|
17
30
|
config_fields: TestFieldDef[];
|
|
18
31
|
cycle_fields: TestFieldDef[];
|
|
19
32
|
results_fields: TestFieldDef[];
|
|
33
|
+
/**
|
|
34
|
+
* AMS asset references resolved at start_test. The form scans these
|
|
35
|
+
* for `select=by_id_field` entries pointing at a config field
|
|
36
|
+
* (`from: "config.<name>"`) and renders that field as a Dropdown of
|
|
37
|
+
* matching AMS assets instead of a free-form text input. No
|
|
38
|
+
* project.json change required — the form derives this from the
|
|
39
|
+
* existing schema.
|
|
40
|
+
*/
|
|
41
|
+
asset_refs?: TisAssetRef[];
|
|
20
42
|
/** Optional pretty label for the Test Method picker. Falls back
|
|
21
43
|
* to the canonical method_id key. */
|
|
22
44
|
label?: string;
|
|
23
45
|
/** Optional long-form description shown in the picker dialog
|
|
24
46
|
* when this method is highlighted. */
|
|
25
47
|
description?: string;
|
|
48
|
+
/** Optional post-cycle analysis dispatch (`{ script, function }`).
|
|
49
|
+
* Consumed server-side by the codegen; the form doesn't render
|
|
50
|
+
* anything based on it but accepts it so generated schema
|
|
51
|
+
* literals from `acctl codegen-tags` typecheck cleanly. */
|
|
52
|
+
analysis?: any;
|
|
53
|
+
/** Free-form view declarations for chart components. Same rationale
|
|
54
|
+
* as `analysis` — the form ignores them; the type just has to
|
|
55
|
+
* accept them so generated schemas typecheck. */
|
|
56
|
+
views?: Record<string, any>;
|
|
26
57
|
}
|
|
27
58
|
/**
|
|
28
59
|
* Test-setup form. Renders Sample ID, Test Method picker, and Test
|
|
@@ -41,4 +72,5 @@ export interface TestSetupFormProps {
|
|
|
41
72
|
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
42
73
|
}
|
|
43
74
|
export declare const TestSetupForm: React.FC<TestSetupFormProps>;
|
|
75
|
+
export {};
|
|
44
76
|
//# sourceMappingURL=TestSetupForm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAcxE;;;;;GAKG;AACH,UAAU,WAAW;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;gEAG4D;IAC5D,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf;;sDAEkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAyED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqUtD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e;export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:a})=>{const i=useTis(),{invoke:o,write:n}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(i.schemas),[i.schemas]),d=i.selection.projectId,m=""!==d.trim()&&i.projectKnown(d.trim()),[p,u]=useState(i.selection.methodId||t||i.defaultMethodId||""),[f,h]=useState(""),
|
|
1
|
+
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dropdown}from"primereact/dropdown";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{useAmsAssets,useAmsRoles}from"../ams/AmsProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:a})=>{const i=useAmsAssets(),o=useAmsRoles(),n=useMemo(()=>i.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=o[e.asset_type]?.find(t=>t.location===e.location)?.label,s=[e.asset_id];return t?s.push(`— ${t}`):e.location&&s.push(`— ${e.location}`),e.serial&&s.push(`(s/n ${e.serial})`),{label:s.join(" "),value:e.asset_id}}),[i,o,e]);return _jsx(Dropdown,{value:t,options:n,onChange:e=>s(e.value??""),placeholder:0===n.length?`No active ${e} assets registered — add one in Settings → Assets`:`Select ${e}…`,className:a?"p-invalid":"",filter:!0,showClear:!0,disabled:0===n.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:a})=>{const i=useTis(),{invoke:o,write:n}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(i.schemas),[i.schemas]),d=i.selection.projectId,m=""!==d.trim()&&i.projectKnown(d.trim()),[p,u]=useState(i.selection.methodId||t||i.defaultMethodId||""),[f,h]=useState(i.selection.sampleId||""),g=i.stagedConfig,x=e=>{const t="function"==typeof e?e(i.stagedConfig):e;t!==i.stagedConfig&&i.setStagedConfig(t)},_=e??(p?i.schemas[p]:void 0);useEffect(()=>{i.selection.methodId!==p&&p&&i.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{i.selection.sampleId!==f&&i.setSelection({sampleId:f})},[f]),useEffect(()=>{i.state.stagedSampleId&&i.state.stagedSampleId!==f&&h(i.state.stagedSampleId)},[i.state.stagedSampleId]),useEffect(()=>{i.selection.methodId&&i.selection.methodId!==p&&u(i.selection.methodId)},[i.selection.methodId]);const[j,v]=useState(!1),[y,I]=useState(!1);useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const a=l(s.source);if(!a)continue;const i=r[a.tagName];null!=i&&(t[s.name]!==i&&(t===e&&(t={...e}),t[s.name]=i))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void v(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),f.trim()||(e=!1),e&&!i.projectFieldsLoaded&&(e=!1);for(const t of _.config_fields)if("sample_id"!==t.name&&t.required){const s=g[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(v(e),a&&a(e,g),e){const{sample_id:e,...t}=g??{},s={...i.projectFields,...t};o("tis.stage_test",MessageType.Request,{project_id:d,method_id:p,sample_id:f,config:s}).catch(e=>{})}},[g,_,d,p,f,m,i.projectFields,i.projectFieldsLoaded,a,o]);const C=async(e,t)=>{if(x({...g,[e.name]:t}),e.source)try{await n(e.source,t)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:i.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});if(!m)return _jsxs("div",{style:{padding:"1.25rem",maxWidth:"600px"},children:[_jsx("h3",{className:"ac-form-section",children:"No project selected"}),_jsxs("p",{style:{color:"var(--text-secondary-color)",marginTop:"0.5rem"},children:["Pick a project on the ",_jsx("strong",{children:"Project"})," tab first",""!==d.trim()&&` (or click + there to create "${d.trim()}")`,"."]})]});return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx("span",{style:{color:j?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:j?"pi pi-check-circle":"pi pi-exclamation-circle"})}),_jsxs("span",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)",fontWeight:"normal",marginLeft:"0.25rem"},children:["project: ",_jsx("strong",{children:d})]})]}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:f,onValueChanged:e=>{h(e)},className:f.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:f.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:f.trim()?"pi pi-check":"pi pi-times"})}),c.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(p,_),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>I(!0),tooltip:c.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:p?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:p?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),_.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=g[e.name];return void 0!==t&&""!==t&&null!==t})(e),s=`acFormInfo_${e.name}`,a=(e=>{const t=_?.asset_refs??[],s=`config.${e.name}`,a=t.find(e=>"by_id_field"===e.select&&e.from===s);return a?a.asset_type:null})(e),i=!a&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),a?_jsx(AssetIdPicker,{assetType:a,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>C(e,t),invalid:!t}):i?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(g[e.name]):null,onValueChanged:t=>C(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>C(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsxs(_Fragment,{children:[_jsx(Tooltip,{target:`#${s}`,position:"left"}),_jsx("span",{id:s,"data-pr-tooltip":e.description,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",cursor:"help"},children:_jsx("i",{className:"pi pi-info-circle",style:{color:"var(--text-secondary-color)"}})})]}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(TestMethodDialog,{visible:y,onHide:()=>I(!1),currentMethodId:p,onSelected:e=>u(e)})]})};
|
|
@@ -77,6 +77,21 @@ export interface TisContextValue {
|
|
|
77
77
|
/** Stash freshly-known project_fields without a round trip — used
|
|
78
78
|
* by the create / edit dialogs after a successful submit. */
|
|
79
79
|
setProjectFields: (id: string, fields: Record<string, any>) => void;
|
|
80
|
+
/**
|
|
81
|
+
* In-progress test draft — the operator's pending Sample ID and
|
|
82
|
+
* config_field values for the active method. Held on the provider
|
|
83
|
+
* (not in `<TestSetupForm>`'s local React state) so the values
|
|
84
|
+
* survive remount when the operator switches tabs. Wiped via
|
|
85
|
+
* `clearStagedConfig()` after a successful start_test or when the
|
|
86
|
+
* operator cancels the staged record.
|
|
87
|
+
*/
|
|
88
|
+
stagedConfig: Record<string, any>;
|
|
89
|
+
/** Merge a partial patch into `stagedConfig`. Existing fields not
|
|
90
|
+
* mentioned in the patch are preserved. */
|
|
91
|
+
setStagedConfig: (patch: Record<string, any>) => void;
|
|
92
|
+
/** Reset `stagedConfig` to `{}`. Called by the form on a fresh
|
|
93
|
+
* method selection or after a run completes. */
|
|
94
|
+
clearStagedConfig: () => void;
|
|
80
95
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
81
96
|
* omitted to aggregate runs across every method in the project —
|
|
82
97
|
* the History tab uses this. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useReducer,useRef,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_STATE={staged:!1,stagedProjectId:"",stagedMethodId:"",stagedSampleId:"",active:!1,activeProjectId:"",activeMethodId:"",activeSampleId:"",activeRunId:""},EMPTY_SELECTION={projectId:"",methodId:"",sampleId:"",runId:""},TisContext=createContext({schemas:{},defaultMethodId:"",schemasLoaded:!1,state:EMPTY_STATE,selection:EMPTY_SELECTION,setSelection:()=>{},existingProjects:[],projectKnown:()=>!1,refreshProjects:async()=>{},markProjectJustCreated:()=>{},projectFields:{},projectFieldsLoaded:!1,loadProjectFields:async()=>null,setProjectFields:()=>{},fetchRuns:async()=>[],fetchRun:async()=>null,runCache:{}});function liveReducer(e,t){switch(t.kind){case"staged":return{...e,staged:t.value};case"staged_project_id":return{...e,stagedProjectId:t.value};case"staged_method_id":return{...e,stagedMethodId:t.value};case"staged_sample_id":return{...e,stagedSampleId:t.value};case"active":return{...e,active:t.value};case"active_project_id":return{...e,activeProjectId:t.value};case"active_method_id":return{...e,activeMethodId:t.value};case"active_sample_id":return{...e,activeSampleId:t.value};case"active_run_id":return{...e,activeRunId:t.value}}}export const TisProvider=({children:e,defaultMethodId:t})=>{const{invoke:s,subscribe:a,unsubscribe:c}=useContext(EventEmitterContext),[r,d]=useState({}),[u
|
|
1
|
+
import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useReducer,useRef,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_STATE={staged:!1,stagedProjectId:"",stagedMethodId:"",stagedSampleId:"",active:!1,activeProjectId:"",activeMethodId:"",activeSampleId:"",activeRunId:""},EMPTY_SELECTION={projectId:"",methodId:"",sampleId:"",runId:""},TisContext=createContext({schemas:{},defaultMethodId:"",schemasLoaded:!1,state:EMPTY_STATE,selection:EMPTY_SELECTION,setSelection:()=>{},existingProjects:[],projectKnown:()=>!1,refreshProjects:async()=>{},markProjectJustCreated:()=>{},projectFields:{},projectFieldsLoaded:!1,loadProjectFields:async()=>null,setProjectFields:()=>{},stagedConfig:{},setStagedConfig:()=>{},clearStagedConfig:()=>{},fetchRuns:async()=>[],fetchRun:async()=>null,runCache:{}});function liveReducer(e,t){switch(t.kind){case"staged":return{...e,staged:t.value};case"staged_project_id":return{...e,stagedProjectId:t.value};case"staged_method_id":return{...e,stagedMethodId:t.value};case"staged_sample_id":return{...e,stagedSampleId:t.value};case"active":return{...e,active:t.value};case"active_project_id":return{...e,activeProjectId:t.value};case"active_method_id":return{...e,activeMethodId:t.value};case"active_sample_id":return{...e,activeSampleId:t.value};case"active_run_id":return{...e,activeRunId:t.value}}}export const TisProvider=({children:e,defaultMethodId:t})=>{const{invoke:s,subscribe:a,unsubscribe:c}=useContext(EventEmitterContext),[r,d]=useState({}),[n,u]=useState(t??""),[i,o]=useState(!1),[l,p]=useReducer(liveReducer,EMPTY_STATE),[_,m]=useState({projectId:null,methodId:null,sampleId:null,runId:null});useEffect(()=>{let e=!1;return(async()=>{try{const a=await s("tis.list_schemas",MessageType.Request,{});if(e)return;if(a?.success&&a.data){const e=a.data.test_methods??{},s=a.data.default_method_id??"";d(e),!t&&s&&u(s),o(!0)}}catch(e){}})(),()=>{e=!0}},[]),useEffect(()=>{const e=[a("tis.staged",e=>p({kind:"staged",value:!!e})),a("tis.staged_project_id",e=>p({kind:"staged_project_id",value:String(e??"")})),a("tis.staged_method_id",e=>p({kind:"staged_method_id",value:String(e??"")})),a("tis.staged_sample_id",e=>p({kind:"staged_sample_id",value:String(e??"")})),a("tis.active",e=>p({kind:"active",value:!!e})),a("tis.active_project_id",e=>p({kind:"active_project_id",value:String(e??"")})),a("tis.active_method_id",e=>p({kind:"active_method_id",value:String(e??"")})),a("tis.active_sample_id",e=>p({kind:"active_sample_id",value:String(e??"")})),a("tis.active_run_id",e=>p({kind:"active_run_id",value:String(e??"")}))];return()=>{e.forEach(c)}},[a,c]);const I=useRef({}),[f,v]=useState(0),h=useCallback(()=>v(e=>e+1),[]),j=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,cycles:[...s.cycles,t]},h()},[h]),g=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,results:t},h()},[h]);useEffect(()=>{const e=a("tis.cycle_added",e=>{e?.run_id&&e.cycle&&j(e.run_id,e.cycle)}),t=a("tis.results_updated",e=>{e?.run_id&&g(e.run_id,e.results??{})});return()=>{c(e),c(t)}},[a,c,j,g]);const C=useMemo(()=>({projectId:_.projectId??l.activeProjectId,methodId:_.methodId??(l.activeMethodId||n),sampleId:_.sampleId??l.activeSampleId,runId:_.runId??l.activeRunId}),[_,l,n]),S=useCallback(e=>{m(t=>({projectId:void 0===e.projectId?t.projectId:e.projectId,methodId:void 0===e.methodId?t.methodId:e.methodId,sampleId:void 0===e.sampleId?t.sampleId:e.sampleId,runId:void 0===e.runId?t.runId:e.runId}))},[]),y=useRef("");useEffect(()=>{const e=l.activeRunId;e&&e!==y.current&&(y.current=e,m({projectId:null,methodId:null,sampleId:null,runId:null}))},[l.activeRunId]);const x=useCallback(async(e,t)=>{if(!e)return[];const a={project_id:e};t&&(a.method_id=t);try{const e=await s("tis.list_tests",MessageType.Request,a);if(e?.success&&e.data?.tests)return e.data.tests}catch(e){}return[]},[s]),T=useCallback(async(e,t,a)=>{if(!e||!t||!a)return null;try{const c=await s("tis.read_test",MessageType.Request,{project_id:e,method_id:t,run_id:a}),r=await s("tis.read_cycles",MessageType.Request,{project_id:e,method_id:t,run_id:a,offset:0,limit:1e3,order:"asc"});if(!c?.success)return null;const d={meta:c.data??null,cycles:r?.success?r.data?.cycles??[]:[],results:c.data?.results??{},rawData:I.current[a]?.rawData??{}};return I.current[a]=d,h(),d}catch(e){return null}},[s,h]),E=useMemo(()=>({...I.current}),[f]),[k,R]=useState([]),M=useRef(new Set),[P,b]=useState(0),[w,F]=useState({}),q=useCallback(async()=>{try{const e=await s("tis.list_projects",MessageType.Request,{});e?.success&&e.data?.projects&&R(e.data.projects)}catch(e){}},[s]),D=useCallback(e=>{e&&(M.current.has(e)||(M.current.add(e),b(e=>e+1)))},[]),L=useCallback(e=>!!e&&(!!M.current.has(e)||k.includes(e)),[k,P]),Y=useCallback((e,t)=>{e&&F(s=>({...s,[e]:t}))},[]),A=useCallback(async e=>{if(!e)return null;try{const t=await s("tis.read_project",MessageType.Request,{project_id:e});if(t?.success){const s=t.data?.project_fields??{};return F(t=>({...t,[e]:s})),s}}catch(e){}return null},[s]);useEffect(()=>{q()},[q]),useEffect(()=>{const e=a("tis.project_created",()=>{q()}),t=a("tis.project_updated",e=>{const t="string"==typeof e?.project_id?e.project_id:"";t&&A(t)});return()=>{c(e),c(t)}},[a,c,q,A]),useEffect(()=>{const e=C.projectId;e&&L(e)&&void 0===w[e]&&A(e)},[C.projectId,L,w,A]);const J=w[C.projectId]??{},K=void 0!==w[C.projectId],[N,O]=useState({}),z=useCallback(e=>{O(t=>({...t,...e}))},[]),B=useCallback(()=>{O({})},[]),G=useMemo(()=>({schemas:r,defaultMethodId:n,schemasLoaded:i,state:l,selection:C,setSelection:S,existingProjects:k,projectKnown:L,refreshProjects:q,markProjectJustCreated:D,projectFields:J,projectFieldsLoaded:K,loadProjectFields:A,setProjectFields:Y,stagedConfig:N,setStagedConfig:z,clearStagedConfig:B,fetchRuns:x,fetchRun:T,runCache:E}),[r,n,i,l,C,S,k,L,q,D,J,K,A,Y,N,z,B,x,T,E]);return _jsx(TisContext.Provider,{value:G,children:e})};export const useTis=()=>useContext(TisContext);export const useTisSchemas=()=>useContext(TisContext).schemas;export const useTisState=()=>useContext(TisContext).state;export const useTisSelection=()=>{const{selection:e,setSelection:t}=useContext(TisContext);return[e,t]};export const useTisRuns=(e,t)=>{const{fetchRuns:s}=useContext(TisContext),[a,c]=useState([]),[r,d]=useState(!1),n=useCallback(async()=>{if(e){d(!0);try{c(await s(e,t))}finally{d(!1)}}else c([])},[e,t,s]);return useEffect(()=>{n()},[n]),{runs:a,loading:r,refresh:n}};export const useTisRun=e=>{const{selection:t,fetchRun:s,runCache:a}=useContext(TisContext),[c,r]=useState(!1),d=e??t.runId;useEffect(()=>{if(!d)return;if(a[d]?.meta)return;const e=t.projectId,c=t.methodId;e&&c&&(r(!0),s(e,c,d).finally(()=>r(!1)))},[d,t.projectId,t.methodId,s,a]);const n=d?a[d]:null;return{meta:n?.meta??null,cycles:n?.cycles??[],results:n?.results??{},rawData:n?.rawData??{},loading:c}};export{TisContext};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { Button } from 'primereact/button';
|
|
3
3
|
import { InputText } from 'primereact/inputtext';
|
|
4
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
4
5
|
import { Tooltip } from 'primereact/tooltip';
|
|
5
6
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
7
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
@@ -8,8 +9,23 @@ import { MessageType } from '../../hub/CommandMessage';
|
|
|
8
9
|
import { ValueInput } from '../ValueInput';
|
|
9
10
|
import { TextInput } from '../TextInput';
|
|
10
11
|
import { useTis } from './TisProvider';
|
|
12
|
+
import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
|
|
11
13
|
import { TestMethodDialog } from './TestMethodDialog';
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* One asset_ref declared on a test method. We only consume the subset
|
|
17
|
+
* the form cares about — `field`, `asset_type`, `select`, `from`,
|
|
18
|
+
* `location`. Other fields (calibration_required, label, description)
|
|
19
|
+
* exist on the wire but aren't form-relevant.
|
|
20
|
+
*/
|
|
21
|
+
interface TisAssetRef {
|
|
22
|
+
field: string;
|
|
23
|
+
asset_type: string;
|
|
24
|
+
select: 'by_location' | 'by_id_field';
|
|
25
|
+
from?: string;
|
|
26
|
+
location?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export interface TestFieldDef {
|
|
14
30
|
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
15
31
|
name: string;
|
|
@@ -29,12 +45,30 @@ export interface TestMethod {
|
|
|
29
45
|
config_fields: TestFieldDef[];
|
|
30
46
|
cycle_fields: TestFieldDef[];
|
|
31
47
|
results_fields: TestFieldDef[];
|
|
48
|
+
/**
|
|
49
|
+
* AMS asset references resolved at start_test. The form scans these
|
|
50
|
+
* for `select=by_id_field` entries pointing at a config field
|
|
51
|
+
* (`from: "config.<name>"`) and renders that field as a Dropdown of
|
|
52
|
+
* matching AMS assets instead of a free-form text input. No
|
|
53
|
+
* project.json change required — the form derives this from the
|
|
54
|
+
* existing schema.
|
|
55
|
+
*/
|
|
56
|
+
asset_refs?: TisAssetRef[];
|
|
32
57
|
/** Optional pretty label for the Test Method picker. Falls back
|
|
33
58
|
* to the canonical method_id key. */
|
|
34
59
|
label?: string;
|
|
35
60
|
/** Optional long-form description shown in the picker dialog
|
|
36
61
|
* when this method is highlighted. */
|
|
37
62
|
description?: string;
|
|
63
|
+
/** Optional post-cycle analysis dispatch (`{ script, function }`).
|
|
64
|
+
* Consumed server-side by the codegen; the form doesn't render
|
|
65
|
+
* anything based on it but accepts it so generated schema
|
|
66
|
+
* literals from `acctl codegen-tags` typecheck cleanly. */
|
|
67
|
+
analysis?: any;
|
|
68
|
+
/** Free-form view declarations for chart components. Same rationale
|
|
69
|
+
* as `analysis` — the form ignores them; the type just has to
|
|
70
|
+
* accept them so generated schemas typecheck. */
|
|
71
|
+
views?: Record<string, any>;
|
|
38
72
|
}
|
|
39
73
|
|
|
40
74
|
/**
|
|
@@ -69,6 +103,60 @@ const hasDescription = (f: TestFieldDef): boolean =>
|
|
|
69
103
|
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
70
104
|
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
71
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Dropdown over the active subset of AMS assets of a given type. Used
|
|
108
|
+
* for config fields that an `asset_ref` resolves via
|
|
109
|
+
* `select=by_id_field` — the operator picks an asset by ID from the
|
|
110
|
+
* registry rather than typing it. The dropdown's option labels
|
|
111
|
+
* include the asset's role (when the asset has a known role) and
|
|
112
|
+
* serial so the operator can identify "the right one" without leaving
|
|
113
|
+
* the form.
|
|
114
|
+
*
|
|
115
|
+
* Falls back to a graceful empty state when the AMS provider isn't
|
|
116
|
+
* mounted (no assets) — reads as "No matching assets registered" so
|
|
117
|
+
* the operator knows the gap and can go register one in Settings.
|
|
118
|
+
*/
|
|
119
|
+
const AssetIdPicker: React.FC<{
|
|
120
|
+
assetType: string;
|
|
121
|
+
value: string;
|
|
122
|
+
onChange: (val: string) => void;
|
|
123
|
+
invalid?: boolean;
|
|
124
|
+
}> = ({ assetType, value, onChange, invalid }) => {
|
|
125
|
+
const assets = useAmsAssets();
|
|
126
|
+
const roles = useAmsRoles();
|
|
127
|
+
|
|
128
|
+
const options = useMemo(() => {
|
|
129
|
+
const filtered = (assets as AmsAssetEntry[])
|
|
130
|
+
.filter(a => a.asset_type === assetType && a.status === 'active');
|
|
131
|
+
return filtered.map(a => {
|
|
132
|
+
const roleLabel = roles[a.asset_type]
|
|
133
|
+
?.find(r => r.location === a.location)?.label;
|
|
134
|
+
const labelParts = [a.asset_id];
|
|
135
|
+
if (roleLabel) labelParts.push(`— ${roleLabel}`);
|
|
136
|
+
else if (a.location) labelParts.push(`— ${a.location}`);
|
|
137
|
+
if (a.serial) labelParts.push(`(s/n ${a.serial})`);
|
|
138
|
+
return { label: labelParts.join(' '), value: a.asset_id };
|
|
139
|
+
});
|
|
140
|
+
}, [assets, roles, assetType]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Dropdown
|
|
144
|
+
value={value}
|
|
145
|
+
options={options}
|
|
146
|
+
onChange={(e) => onChange(e.value ?? '')}
|
|
147
|
+
placeholder={
|
|
148
|
+
options.length === 0
|
|
149
|
+
? `No active ${assetType} assets registered — add one in Settings → Assets`
|
|
150
|
+
: `Select ${assetType}…`
|
|
151
|
+
}
|
|
152
|
+
className={invalid ? 'p-invalid' : ''}
|
|
153
|
+
filter
|
|
154
|
+
showClear
|
|
155
|
+
disabled={options.length === 0}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
72
160
|
// -------------------------------------------------------------------------
|
|
73
161
|
|
|
74
162
|
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
@@ -92,8 +180,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
92
180
|
const [methodId, setMethodIdLocal] = useState<string>(
|
|
93
181
|
tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
|
|
94
182
|
);
|
|
95
|
-
|
|
96
|
-
|
|
183
|
+
// Sample ID + config_field values come from the TisProvider rather
|
|
184
|
+
// than local React state so they survive form unmount when the
|
|
185
|
+
// operator switches tabs. Initial value reads from selection /
|
|
186
|
+
// stagedConfig; subsequent edits write back to the provider.
|
|
187
|
+
const [sampleId, setSampleIdLocal] = useState<string>(tis.selection.sampleId || '');
|
|
188
|
+
const config = tis.stagedConfig;
|
|
189
|
+
const setConfig = (updater: any) => {
|
|
190
|
+
const next = typeof updater === 'function' ? updater(tis.stagedConfig) : updater;
|
|
191
|
+
if (next !== tis.stagedConfig) {
|
|
192
|
+
tis.setStagedConfig(next);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
97
195
|
|
|
98
196
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
99
197
|
|
|
@@ -218,15 +316,39 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
218
316
|
}
|
|
219
317
|
};
|
|
220
318
|
|
|
319
|
+
/**
|
|
320
|
+
* If the test method has an `asset_ref` whose `select=by_id_field`
|
|
321
|
+
* pulls from this config field, return the asset_type the field
|
|
322
|
+
* should pick from. The form renders that field as a dropdown of
|
|
323
|
+
* AMS assets instead of a free-form text input.
|
|
324
|
+
*
|
|
325
|
+
* The schema already encodes the relationship via `from:
|
|
326
|
+
* "config.<field_name>"` — no project.json change required.
|
|
327
|
+
*/
|
|
328
|
+
const assetTypeForField = (field: TestFieldDef): string | null => {
|
|
329
|
+
const refs = (schema?.asset_refs ?? []) as TisAssetRef[];
|
|
330
|
+
const expectedFrom = `config.${field.name}`;
|
|
331
|
+
const hit = refs.find(r => r.select === 'by_id_field' && r.from === expectedFrom);
|
|
332
|
+
return hit ? hit.asset_type : null;
|
|
333
|
+
};
|
|
334
|
+
|
|
221
335
|
const renderConfigField = (field: TestFieldDef) => {
|
|
222
336
|
if (field.name === 'sample_id') return null;
|
|
223
337
|
const valid = isFieldValid(field);
|
|
224
|
-
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
225
338
|
const tooltipId = `acFormInfo_${field.name}`;
|
|
339
|
+
const assetType = assetTypeForField(field);
|
|
340
|
+
const isNum = !assetType && field.type !== 'string' && field.type !== 'bool';
|
|
226
341
|
return (
|
|
227
342
|
<React.Fragment key={field.name}>
|
|
228
343
|
<span className="ac-form-label">{labelOf(field)}</span>
|
|
229
|
-
{
|
|
344
|
+
{assetType ? (
|
|
345
|
+
<AssetIdPicker
|
|
346
|
+
assetType={assetType}
|
|
347
|
+
value={config[field.name] != null ? String(config[field.name]) : ''}
|
|
348
|
+
onChange={(val) => handleFieldChange(field, val)}
|
|
349
|
+
invalid={!valid}
|
|
350
|
+
/>
|
|
351
|
+
) : isNum ? (
|
|
230
352
|
<ValueInput
|
|
231
353
|
label={undefined}
|
|
232
354
|
value={config[field.name] != null ? Number(config[field.name]) : null}
|
|
@@ -129,6 +129,22 @@ export interface TisContextValue {
|
|
|
129
129
|
* by the create / edit dialogs after a successful submit. */
|
|
130
130
|
setProjectFields: (id: string, fields: Record<string, any>) => void;
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* In-progress test draft — the operator's pending Sample ID and
|
|
134
|
+
* config_field values for the active method. Held on the provider
|
|
135
|
+
* (not in `<TestSetupForm>`'s local React state) so the values
|
|
136
|
+
* survive remount when the operator switches tabs. Wiped via
|
|
137
|
+
* `clearStagedConfig()` after a successful start_test or when the
|
|
138
|
+
* operator cancels the staged record.
|
|
139
|
+
*/
|
|
140
|
+
stagedConfig: Record<string, any>;
|
|
141
|
+
/** Merge a partial patch into `stagedConfig`. Existing fields not
|
|
142
|
+
* mentioned in the patch are preserved. */
|
|
143
|
+
setStagedConfig: (patch: Record<string, any>) => void;
|
|
144
|
+
/** Reset `stagedConfig` to `{}`. Called by the form on a fresh
|
|
145
|
+
* method selection or after a run completes. */
|
|
146
|
+
clearStagedConfig: () => void;
|
|
147
|
+
|
|
132
148
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
133
149
|
* omitted to aggregate runs across every method in the project —
|
|
134
150
|
* the History tab uses this. */
|
|
@@ -167,6 +183,9 @@ const TisContext = createContext<TisContextValue>({
|
|
|
167
183
|
projectFieldsLoaded: false,
|
|
168
184
|
loadProjectFields: async () => null,
|
|
169
185
|
setProjectFields: () => {},
|
|
186
|
+
stagedConfig: {},
|
|
187
|
+
setStagedConfig: () => {},
|
|
188
|
+
clearStagedConfig: () => {},
|
|
170
189
|
fetchRuns: async () => [],
|
|
171
190
|
fetchRun: async () => null,
|
|
172
191
|
runCache: {},
|
|
@@ -506,17 +525,32 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
506
525
|
const projectFields = projectFieldsCache[selection.projectId] ?? {};
|
|
507
526
|
const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
|
|
508
527
|
|
|
528
|
+
// Operator's pending Sample ID and config_field values live here
|
|
529
|
+
// (rather than as local state in <TestSetupForm>) so they survive
|
|
530
|
+
// the form unmounting when the user switches to another tab. The
|
|
531
|
+
// form re-hydrates from this on remount, so leaving the Test tab
|
|
532
|
+
// and coming back is transparent.
|
|
533
|
+
const [stagedConfig, setStagedConfigState] = useState<Record<string, any>>({});
|
|
534
|
+
const setStagedConfig = useCallback((patch: Record<string, any>) => {
|
|
535
|
+
setStagedConfigState(prev => ({ ...prev, ...patch }));
|
|
536
|
+
}, []);
|
|
537
|
+
const clearStagedConfig = useCallback(() => {
|
|
538
|
+
setStagedConfigState({});
|
|
539
|
+
}, []);
|
|
540
|
+
|
|
509
541
|
const value: TisContextValue = useMemo(() => ({
|
|
510
542
|
schemas, defaultMethodId, schemasLoaded,
|
|
511
543
|
state, selection, setSelection,
|
|
512
544
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
513
545
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
546
|
+
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
514
547
|
fetchRuns, fetchRun, runCache,
|
|
515
548
|
}), [
|
|
516
549
|
schemas, defaultMethodId, schemasLoaded,
|
|
517
550
|
state, selection, setSelection,
|
|
518
551
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
519
552
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
553
|
+
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
520
554
|
fetchRuns, fetchRun, runCache,
|
|
521
555
|
]);
|
|
522
556
|
|