@adcops/autocore-react 3.3.101 → 3.3.106

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.
@@ -45,6 +45,11 @@ export interface TestFieldDef {
45
45
  * Cycle and results values are scaled by the corresponding paths
46
46
  * in TestDataView; the server scales CSV exports too. */
47
47
  scale?: number;
48
+ /** Optional inclusive bounds (display units, same convention as `default`
49
+ * and `scale`) for the operator's numeric entry. The numeric input
50
+ * rejects values outside `[min, max]`; non-numeric fields ignore them. */
51
+ min?: number;
52
+ max?: number;
48
53
  /** Optional fixed set of choices. When present, the field renders
49
54
  * as a dropdown and the operator must pick one of the declared
50
55
  * values rather than typing freely. Each entry is either a bare
@@ -1 +1 @@
1
- {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAexE;;;;;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;IACrB;;;;;;;iEAO6D;IAC7D,OAAO,CAAC,EAAE,GAAG,CAAC;IACd;;;;;;;;;;8DAU0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;6CAMyC;IACzC,OAAO,CAAC,EAAE,KAAK,CACT,MAAM,GACN,MAAM,GACN,OAAO,GACP;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;KAAE,CACzD,CAAC;CACL;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;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACxC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;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;AA+GD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAymBtD,CAAC"}
1
+ {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAexE;;;;;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;IACrB;;;;;;;iEAO6D;IAC7D,OAAO,CAAC,EAAE,GAAG,CAAC;IACd;;;;;;;;;;8DAU0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;+EAE2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;6CAMyC;IACzC,OAAO,CAAC,EAAE,KAAK,CACT,MAAM,GACN,MAAM,GACN,OAAO,GACP;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;KAAE,CACzD,CAAC;CACL;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;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACxC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;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;AAuKD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAynBtD,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}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{Dialog}from"primereact/dialog";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";import{ConfigurationDialog,configLabelOf}from"./ConfigurationDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},rawToDisplay=(e,t)=>t&&1!==t&&"number"==typeof e&&Number.isFinite(e)?e*t:e,displayToRaw=(e,t)=>t&&1!==t&&"number"==typeof e&&Number.isFinite(e)?e/t:e,hasDescription=e=>"string"==typeof e.description&&e.description.length>0,normalizeOptions=e=>(e??[]).map(e=>null!==e&&"object"==typeof e?{label:String(e.label??e.value),value:e.value}:{label:String(e),value:e}),methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:i})=>{const a=useAmsAssets(),o=useAmsRoles(),n=useMemo(()=>a.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}}),[a,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:"ac-dropdown-clearable"+(i?" p-invalid":""),filter:!0,showClear:!0,disabled:0===n.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:i})=>{const a=useTis(),{invoke:o,write:n}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(a.schemas),[a.schemas]),d=a.selection.projectId,m=""!==d.trim()&&a.projectKnown(d.trim()),[p,u]=useState(a.selection.methodId||t||a.defaultMethodId||""),[f,h]=useState(a.selection.sampleId||""),g=a.stagedConfig,x=e=>{const t="function"==typeof e?e(a.stagedConfig):e;t!==a.stagedConfig&&a.setStagedConfig(t)},_=e??(p?a.schemas[p]:void 0),j=(e,t)=>{for(const[s,i]of Object.entries(e.defaults??{})){if("sample_id"===s)continue;const e=_?.config_fields.find(e=>e.name===s),a=displayToRaw(i,e?.scale);t[s]=a,e?.source&&Promise.resolve().then(()=>n(e.source,a)).catch(e=>{})}return t};useEffect(()=>{a.selection.methodId!==p&&p&&a.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{a.selection.sampleId!==f&&a.setSelection({sampleId:f})},[f]),useEffect(()=>{a.state.stagedSampleId&&a.state.stagedSampleId!==f&&h(a.state.stagedSampleId)},[a.state.stagedSampleId]),useEffect(()=>{a.selection.methodId&&a.selection.methodId!==p&&u(a.selection.methodId)},[a.selection.methodId]);const[y,b]=useState(!1),[v,C]=useState(!1),[N,I]=useState(!1),T=_?.configurations??[],S=T.find(e=>e.name===a.configurationName),[w,A]=useState({open:!1,title:"",body:null});useEffect(()=>{if(!_||!p)return;if(a.defaultsAppliedForMethod===p)return;a.markDefaultsAppliedForMethod(p);const e=_.configurations&&_.configurations.length>0?_.configurations[0]:void 0;x(t=>{let s=t;for(const e of _.config_fields){if("sample_id"===e.name)continue;if(void 0===e.default||null===e.default)continue;s===t&&(s={...t});const i=displayToRaw(e.default,e.scale);s[e.name]=i,e.source&&Promise.resolve().then(()=>n(e.source,i)).catch(e=>{})}return e&&(s===t&&(s={...t}),s=j(e,s)),s}),a.setConfigurationName(e?e.name:"")},[_,p,n,a.defaultsAppliedForMethod,a.markDefaultsAppliedForMethod,a.setConfigurationName]),useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const i=l(s.source);if(!i)continue;const a=r[i.tagName];null!=a&&(t[s.name]!==a&&(t===e&&(t={...e}),t[s.name]=a))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void b(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),f.trim()||(e=!1),e&&!a.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(b(e),i&&i(e,g),e){const{sample_id:e,...t}=g??{},s={...a.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,a.projectFields,a.projectFieldsLoaded,i,o]);const k=async(e,t)=>{const s=displayToRaw(t,e.scale);if(x({...g,[e.name]:s}),e.source)try{await n(e.source,s)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:a.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()}")`,"."]})]});const F={padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},M=y&&!a.state.lastStartError;return _jsxs("div",{className:"ac-test-setup-form",children:[_jsxs("div",{className:"ac-form-grid ac-test-setup-form__head",style:F,children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx(Button,{icon:M?"pi pi-check-circle":"pi pi-exclamation-circle",severity:M?"success":"danger",text:!0,rounded:!0,"aria-label":"Test Setup status",onClick:()=>{const e=(()=>{const e=[];if(m||e.push("No project is selected. Pick or create one on the Project tab."),a.projectFieldsLoaded||e.push("Project fields are still loading."),p.trim()||e.push("No test method selected."),f.trim()||e.push("Sample ID is required."),_)for(const t of _.config_fields){if("sample_id"===t.name)continue;if(!t.required)continue;const s=g[t.name];void 0!==s&&""!==s&&null!==s||e.push(`Required field "${labelOf(t)}" is empty.`)}return e})(),t=a.state.lastStartError?.trim()??"";let s;s=0!==e.length||t?_jsxs("div",{children:[e.length>0&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"The form is incomplete:"}),_jsx("ul",{style:{margin:"0 0 1rem 1.25rem"},children:e.map((e,t)=>_jsx("li",{children:e},t))})]}),t&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.25rem 0",fontWeight:600},children:"Last start_test error from the server:"}),_jsx("pre",{style:{margin:0,padding:"0.5rem",background:"rgba(0,0,0,0.25)",borderRadius:"4px",whiteSpace:"pre-wrap",fontSize:"0.875rem"},children:t})]})]}):_jsx("p",{style:{margin:0},children:"All required fields are complete. The test is staged and ready to start."}),A({open:!0,title:"Test Setup Status",body:s})}}),_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-folder",type:"button",onClick:()=>C(!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"})})]}),T.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Configuration"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:S?configLabelOf(S):"",placeholder:"No configuration selected",readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-folder",type:"button",onClick:()=>I(!0),tooltip:"Change configuration",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:S?"var(--green-500)":"var(--text-secondary-color)",display:"flex",alignItems:"center"},children:_jsx("i",{className:S?"pi pi-check":"pi pi-minus"})})]})]}),_jsx("div",{className:"ac-test-setup-form__body",children:_jsxs("div",{className:"ac-form-grid",style:F,children:[_jsx("h3",{className:"ac-form-section",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=(e=>{const t=_?.asset_refs??[],s=a.projectAssetRefs??[],i=`config.${e.name}`,o=t.find(e=>"by_id_field"===e.select&&e.from===i);if(o)return o.asset_type;const n=s.find(e=>"by_id_field"===e.select&&e.from===i);return n?n.asset_type:null})(e),i=!s&&Array.isArray(e.options)&&e.options.length>0?normalizeOptions(e.options):null,o=!s&&!i&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(AssetIdPicker,{assetType:s,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>k(e,t),invalid:!t}):i?_jsx(Dropdown,{value:g[e.name]??null,options:i,onChange:t=>k(e,t.value),placeholder:`Select ${e.label??e.name}…`,className:"ac-dropdown-clearable"+(t?"":" p-invalid"),showClear:!e.required}):o?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(rawToDisplay(Number(g[e.name]),e.scale)):null,onValueChanged:t=>k(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>k(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(e)}`,onClick:()=>A({open:!0,title:labelOf(e),body:_jsx("p",{style:{margin:0,whiteSpace:"pre-wrap"},children:e.description})})}):_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:v,onHide:()=>C(!1),currentMethodId:p,onSelected:e=>u(e)}),_jsx(ConfigurationDialog,{visible:N,onHide:()=>I(!1),configurations:T,currentConfigName:a.configurationName,onSelected:e=>{const t=T.find(t=>t.name===e);t&&(x(e=>j(t,{...e})),a.setConfigurationName(e))}}),_jsx(Dialog,{header:w.title,visible:w.open,onHide:()=>A({open:!1,title:"",body:null}),style:{width:"32rem",maxWidth:"90vw"},modal:!0,dismissableMask:!0,children:w.body})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}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{Dialog}from"primereact/dialog";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";import{ConfigurationDialog,configLabelOf}from"./ConfigurationDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},rawToDisplay=(e,t)=>t&&1!==t&&"number"==typeof e&&Number.isFinite(e)?e*t:e,displayToRaw=(e,t)=>t&&1!==t&&"number"==typeof e&&Number.isFinite(e)?e/t:e,hasDescription=e=>"string"==typeof e.description&&e.description.length>0,DEFAULT_TOKEN_RE=/^\$\{\s*([^}]+?)\s*\}$/,resolveDefaultRaw=(e,t,s)=>{const i=e.default;if("string"==typeof i){const e=i.match(DEFAULT_TOKEN_RE);if(e){const i=t(e[1]),a=i?s[i.tagName]:void 0;return null==a?{raw:void 0,ok:!1}:{raw:a,ok:!0}}}return{raw:displayToRaw(i,e.scale),ok:!0}},rangeIssue=(e,t)=>{if(void 0===t||""===t||null===t)return null;if(null==e.min&&null==e.max)return null;const s=Number(rawToDisplay(Number(t),e.scale));return Number.isFinite(s)?null!=e.min&&s<e.min?`must be ≥ ${e.min}`:null!=e.max&&s>e.max?`must be ≤ ${e.max}`:null:null},normalizeOptions=e=>(e??[]).map(e=>null!==e&&"object"==typeof e?{label:String(e.label??e.value),value:e.value}:{label:String(e),value:e}),methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:i})=>{const a=useAmsAssets(),n=useAmsRoles(),o=useMemo(()=>a.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=n[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}}),[a,n,e]);return _jsx(Dropdown,{value:t,options:o,onChange:e=>s(e.value??""),placeholder:0===o.length?`No active ${e} assets registered — add one in Settings → Assets`:`Select ${e}…`,className:"ac-dropdown-clearable"+(i?" p-invalid":""),filter:!0,showClear:!0,disabled:0===o.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:i})=>{const a=useTis(),{invoke:n,write:o}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(a.schemas),[a.schemas]),d=a.selection.projectId,m=""!==d.trim()&&a.projectKnown(d.trim()),[u,p]=useState(a.selection.methodId||t||a.defaultMethodId||""),[f,h]=useState(a.selection.sampleId||""),g=a.stagedConfig,x=e=>{const t="function"==typeof e?e(a.stagedConfig):e;t!==a.stagedConfig&&a.setStagedConfig(t)},_=e??(u?a.schemas[u]:void 0),j=(e,t)=>{for(const[s,i]of Object.entries(e.defaults??{})){if("sample_id"===s)continue;const e=_?.config_fields.find(e=>e.name===s),a=displayToRaw(i,e?.scale);t[s]=a,e?.source&&Promise.resolve().then(()=>o(e.source,a)).catch(e=>{})}return t};useEffect(()=>{a.selection.methodId!==u&&u&&a.setSelection({methodId:u}),s&&s(u)},[u]),useEffect(()=>{a.selection.sampleId!==f&&a.setSelection({sampleId:f})},[f]),useEffect(()=>{a.state.stagedSampleId&&a.state.stagedSampleId!==f&&h(a.state.stagedSampleId)},[a.state.stagedSampleId]),useEffect(()=>{a.selection.methodId&&a.selection.methodId!==u&&p(a.selection.methodId)},[a.selection.methodId]);const[b,y]=useState(!1),[v,N]=useState(!1),[C,I]=useState(!1),T=_?.configurations??[],S=T.find(e=>e.name===a.configurationName),[w,k]=useState({open:!1,title:"",body:null});useEffect(()=>{if(!_||!u)return;if(a.defaultsAppliedForMethod===u)return;a.markDefaultsAppliedForMethod(u);const e=_.configurations&&_.configurations.length>0?_.configurations[0]:void 0;x(t=>{let s=t;for(const e of _.config_fields){if("sample_id"===e.name)continue;if(void 0===e.default||null===e.default)continue;const{raw:i,ok:a}=resolveDefaultRaw(e,l,r);a&&(s===t&&(s={...t}),s[e.name]=i,e.source&&Promise.resolve().then(()=>o(e.source,i)).catch(e=>{}))}return e&&(s===t&&(s={...t}),s=j(e,s)),s}),a.setConfigurationName(e?e.name:"")},[_,u,o,r,l,a.defaultsAppliedForMethod,a.markDefaultsAppliedForMethod,a.setConfigurationName]),useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const i=l(s.source);if(!i)continue;const a=r[i.tagName];null!=a&&(t[s.name]!==a&&(t===e&&(t={...e}),t[s.name]=a))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void y(!1);let e=!0;m||(e=!1),u.trim()||(e=!1),f.trim()||(e=!1),e&&!a.projectFieldsLoaded&&(e=!1);for(const t of _.config_fields){if("sample_id"===t.name)continue;const s=g[t.name],i=void 0===s||""===s||null===s;if(t.required&&i){e=!1;break}if(rangeIssue(t,s)){e=!1;break}}if(y(e),i&&i(e,g),e){const{sample_id:e,...t}=g??{},s={...a.projectFields,...t};n("tis.stage_test",MessageType.Request,{project_id:d,method_id:u,sample_id:f,config:s}).catch(e=>{})}},[g,_,d,u,f,m,a.projectFields,a.projectFieldsLoaded,i,n]);const A=async(e,t)=>{const s=displayToRaw(t,e.scale);if(x({...g,[e.name]:s}),e.source)try{await o(e.source,s)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:a.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()}")`,"."]})]});const D={padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},F=b&&!a.state.lastStartError;return _jsxs("div",{className:"ac-test-setup-form",children:[_jsxs("div",{className:"ac-form-grid ac-test-setup-form__head",style:D,children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx(Button,{icon:F?"pi pi-check-circle":"pi pi-exclamation-circle",severity:F?"success":"danger",text:!0,rounded:!0,"aria-label":"Test Setup status",onClick:()=>{const e=(()=>{const e=[];if(m||e.push("No project is selected. Pick or create one on the Project tab."),a.projectFieldsLoaded||e.push("Project fields are still loading."),u.trim()||e.push("No test method selected."),f.trim()||e.push("Sample ID is required."),_)for(const t of _.config_fields){if("sample_id"===t.name)continue;const s=g[t.name],i=void 0===s||""===s||null===s;if(t.required&&i){e.push(`Required field "${labelOf(t)}" is empty.`);continue}const a=rangeIssue(t,s);a&&e.push(`"${labelOf(t)}" ${a}.`)}return e})(),t=a.state.lastStartError?.trim()??"";let s;s=0!==e.length||t?_jsxs("div",{children:[e.length>0&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"The form is incomplete:"}),_jsx("ul",{style:{margin:"0 0 1rem 1.25rem"},children:e.map((e,t)=>_jsx("li",{children:e},t))})]}),t&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.25rem 0",fontWeight:600},children:"Last start_test error from the server:"}),_jsx("pre",{style:{margin:0,padding:"0.5rem",background:"rgba(0,0,0,0.25)",borderRadius:"4px",whiteSpace:"pre-wrap",fontSize:"0.875rem"},children:t})]})]}):_jsx("p",{style:{margin:0},children:"All required fields are complete. The test is staged and ready to start."}),k({open:!0,title:"Test Setup Status",body:s})}}),_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(u,_),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-folder",type:"button",onClick:()=>N(!0),tooltip:c.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:u?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:u?"pi pi-check":"pi pi-times"})})]}),T.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Configuration"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:S?configLabelOf(S):"",placeholder:"No configuration selected",readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-folder",type:"button",onClick:()=>I(!0),tooltip:"Change configuration",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:S?"var(--green-500)":"var(--text-secondary-color)",display:"flex",alignItems:"center"},children:_jsx("i",{className:S?"pi pi-check":"pi pi-minus"})})]})]}),_jsx("div",{className:"ac-test-setup-form__body",children:_jsxs("div",{className:"ac-form-grid",style:D,children:[_jsx("h3",{className:"ac-form-section",children:"Test Configuration"}),_.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{const t=g[e.name],s=void 0===t||""===t||null===t;return!(e.required&&s||rangeIssue(e,t))})(e),s=(e=>{const t=_?.asset_refs??[],s=a.projectAssetRefs??[],i=`config.${e.name}`,n=t.find(e=>"by_id_field"===e.select&&e.from===i);if(n)return n.asset_type;const o=s.find(e=>"by_id_field"===e.select&&e.from===i);return o?o.asset_type:null})(e),i=!s&&Array.isArray(e.options)&&e.options.length>0?normalizeOptions(e.options):null,n=!s&&!i&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(AssetIdPicker,{assetType:s,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>A(e,t),invalid:!t}):i?_jsx(Dropdown,{value:g[e.name]??null,options:i,onChange:t=>A(e,t.value),placeholder:`Select ${e.label??e.name}…`,className:"ac-dropdown-clearable"+(t?"":" p-invalid"),showClear:!e.required}):n?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(rawToDisplay(Number(g[e.name]),e.scale)):null,min:e.min,max:e.max,onValueChanged:t=>A(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>A(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(e)}`,onClick:()=>k({open:!0,title:labelOf(e),body:_jsx("p",{style:{margin:0,whiteSpace:"pre-wrap"},children:e.description})})}):_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:v,onHide:()=>N(!1),currentMethodId:u,onSelected:e=>p(e)}),_jsx(ConfigurationDialog,{visible:C,onHide:()=>I(!1),configurations:T,currentConfigName:a.configurationName,onSelected:e=>{const t=T.find(t=>t.name===e);t&&(x(e=>j(t,{...e})),a.setConfigurationName(e))}}),_jsx(Dialog,{header:w.title,visible:w.open,onHide:()=>k({open:!1,title:"",body:null}),style:{width:"32rem",maxWidth:"90vw"},modal:!0,dismissableMask:!0,children:w.body})]})};
@@ -1 +1 @@
1
- {"version":3,"file":"TisConfigEditor.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/TisConfigEditor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM5E,OAAO,uBAAuB,CAAC;AAE/B,MAAM,WAAW,oBAAoB;IACjC;+EAC2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAkBD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAoQ1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"TisConfigEditor.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/TisConfigEditor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM5E,OAAO,uBAAuB,CAAC;AAE/B,MAAM,WAAW,oBAAoB;IACjC;+EAC2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAkBD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAyQ1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import{useEffect,useMemo,useState}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{useContext}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTisConfig}from"../../hooks/useTisConfig";import{useAmsAssetTypes}from"../../hooks/useAmsAssetTypes";import{MethodFormEditor}from"./editor/MethodFormEditor";import{SaveDiffDialog}from"./editor/SaveDiffDialog";import"./TisConfigEditor.css";const EMPTY_METHOD={label:"",description:"",project_fields:[],config_fields:[],cycle_fields:[],results_fields:[],views:{},asset_refs:[]};export const TisConfigEditor=({projectId:e,invoker:t})=>{const s=useContext(EventEmitterContext),i=t??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),o=useTisConfig(e,{invoker:i}),a=useAmsAssetTypes({invoker:i}),[n,r]=useState(null),[l,d]=useState(null),[c,m]=useState(!1),[f,p]=useState(!1),[h,u]=useState(!1),[_,g]=useState(""),x=useMemo(()=>o.config?Object.entries(o.config.methods).map(([e,t])=>({id:e,label:t?.label??""})):[],[o.config]);useEffect(()=>{if(!n&&o.config){const e=o.config.defaultMethodId||Object.keys(o.config.methods)[0]||null;r(e)}},[o.config,n]),useEffect(()=>{d(null)},[n]);return _jsxs("div",{className:"tis-editor",children:[_jsxs("header",{className:"tis-editor__header",children:[_jsxs("h2",{children:["Test Methods"," ",o.config?.dirty&&_jsx("span",{className:"tis-editor__dirty-pill",children:"unsaved"})]}),_jsxs("div",{className:"tis-editor__header-actions",children:[_jsx(Button,{label:"Save…",icon:"pi pi-save",disabled:c||!o.config?.dirty,onClick:()=>p(!0)}),_jsx(Button,{label:"Revert",icon:"pi pi-undo",className:"p-button-secondary",disabled:c||!o.config?.dirty,onClick:async()=>{if(window.confirm("Discard all in-progress edits? This cannot be undone.")){m(!0);try{await o.revert()}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),o.error&&_jsxs("div",{className:"tis-editor__error",children:[_jsx("strong",{children:"Error:"})," ",_jsx("pre",{children:o.error})]}),_jsxs("div",{className:"tis-editor__body",children:[_jsxs("aside",{className:"tis-editor__sidebar",children:[_jsxs("div",{className:"tis-editor__sidebar-actions",children:[_jsx(Button,{label:"New",icon:"pi pi-plus",disabled:c,onClick:()=>u(!0)}),_jsx(Button,{label:"Duplicate",icon:"pi pi-clone",className:"p-button-secondary",disabled:c||!n,onClick:async()=>{if(!n||!o.config)return;const e=o.config.methods[n];if(!e)return;let t=`${n}_copy`,s=2;for(;o.config.methods[t];)t=`${n}_copy_${s++}`;m(!0);try{await o.putMethod(t,JSON.parse(JSON.stringify(e))),r(t)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}),_jsx(Button,{label:"Delete",icon:"pi pi-trash",className:"p-button-danger",disabled:c||!n,onClick:async()=>{if(n&&window.confirm(`Remove method "${n}"? This is staged — Save persists it.`)){m(!0);try{await o.removeMethod(n),r(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]}),_jsxs(DataTable,{value:x,selection:x.find(e=>e.id===n)??null,onSelectionChange:e=>r(e.value?.id??null),selectionMode:"single",dataKey:"id",scrollable:!0,scrollHeight:"flex",emptyMessage:o.loading?"Loading…":"No test methods defined.",children:[_jsx(Column,{field:"id",header:"Method ID"}),_jsx(Column,{field:"label",header:"Label"})]})]}),_jsx("section",{className:"tis-editor__detail",children:n&&o.config?.methods[n]?_jsxs(_Fragment,{children:[l&&_jsx("div",{className:"tis-editor__error",children:_jsx("pre",{children:l})}),_jsx(MethodFormEditor,{methodId:n,method:o.config.methods[n],onApply:async e=>{if(n){m(!0);try{await o.putMethod(n,e),d(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}},busy:c,knownAssetTypes:a.types})]}):_jsx("div",{className:"tis-editor__empty",children:"Select a test method on the left, or create a new one."})})]}),_jsxs(Dialog,{header:"New Test Method",visible:h,onHide:()=>u(!1),style:{width:"24rem"},children:[_jsxs("label",{className:"tis-editor__new-method-label",children:["Method ID",_jsx(InputText,{value:_,onChange:e=>g(e.target.value),placeholder:"e.g. translational_traction",autoFocus:!0})]}),_jsx("small",{children:"Canonical key — appears in wire payloads, on-disk paths, and generated code."}),_jsxs("div",{style:{display:"flex",gap:"0.5rem",justifyContent:"flex-end",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:()=>u(!1)}),_jsx(Button,{label:"Create",disabled:!_.trim()||c,onClick:async()=>{const e=_.trim();if(e)if(o.config?.methods[e])d(`A method named "${e}" already exists.`);else{m(!0);try{await o.putMethod(e,EMPTY_METHOD),r(e),u(!1),g("")}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),_jsx(SaveDiffDialog,{visible:f,staged:o.config?.methods??{},invoker:i,onConfirm:async()=>{m(!0);try{await o.save(),p(!1)}catch(e){d(String(e?.message??e))}finally{m(!1)}},onCancel:()=>p(!1)})]})};export default TisConfigEditor;
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import{useEffect,useMemo,useState}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{useContext}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTisConfig}from"../../hooks/useTisConfig";import{useAmsAssetTypes}from"../../hooks/useAmsAssetTypes";import{MethodFormEditor}from"./editor/MethodFormEditor";import{SaveDiffDialog}from"./editor/SaveDiffDialog";import"./TisConfigEditor.css";const EMPTY_METHOD={label:"",description:"",project_fields:[],config_fields:[],cycle_fields:[],results_fields:[],views:{},asset_refs:[]};export const TisConfigEditor=({projectId:e,invoker:t})=>{const s=useContext(EventEmitterContext),i=useMemo(()=>t??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),[t,s]),o=useTisConfig(e,{invoker:i}),a=useAmsAssetTypes({invoker:i}),[n,r]=useState(null),[l,d]=useState(null),[c,m]=useState(!1),[f,p]=useState(!1),[h,u]=useState(!1),[_,g]=useState(""),x=useMemo(()=>o.config?Object.entries(o.config.methods).map(([e,t])=>({id:e,label:t?.label??""})):[],[o.config]);useEffect(()=>{if(!n&&o.config){const e=o.config.defaultMethodId||Object.keys(o.config.methods)[0]||null;r(e)}},[o.config,n]),useEffect(()=>{d(null)},[n]);return _jsxs("div",{className:"tis-editor",children:[_jsxs("header",{className:"tis-editor__header",children:[_jsxs("h2",{children:["Test Methods"," ",o.config?.dirty&&_jsx("span",{className:"tis-editor__dirty-pill",children:"unsaved"})]}),_jsxs("div",{className:"tis-editor__header-actions",children:[_jsx(Button,{label:"Save…",icon:"pi pi-save",disabled:c||!o.config?.dirty,onClick:()=>p(!0)}),_jsx(Button,{label:"Revert",icon:"pi pi-undo",className:"p-button-secondary",disabled:c||!o.config?.dirty,onClick:async()=>{if(window.confirm("Discard all in-progress edits? This cannot be undone.")){m(!0);try{await o.revert()}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),o.error&&_jsxs("div",{className:"tis-editor__error",children:[_jsx("strong",{children:"Error:"})," ",_jsx("pre",{children:o.error})]}),_jsxs("div",{className:"tis-editor__body",children:[_jsxs("aside",{className:"tis-editor__sidebar",children:[_jsxs("div",{className:"tis-editor__sidebar-actions",children:[_jsx(Button,{label:"New",icon:"pi pi-plus",disabled:c,onClick:()=>u(!0)}),_jsx(Button,{label:"Duplicate",icon:"pi pi-clone",className:"p-button-secondary",disabled:c||!n,onClick:async()=>{if(!n||!o.config)return;const e=o.config.methods[n];if(!e)return;let t=`${n}_copy`,s=2;for(;o.config.methods[t];)t=`${n}_copy_${s++}`;m(!0);try{await o.putMethod(t,JSON.parse(JSON.stringify(e))),r(t)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}),_jsx(Button,{label:"Delete",icon:"pi pi-trash",className:"p-button-danger",disabled:c||!n,onClick:async()=>{if(n&&window.confirm(`Remove method "${n}"? This is staged — Save persists it.`)){m(!0);try{await o.removeMethod(n),r(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]}),_jsxs(DataTable,{value:x,selection:x.find(e=>e.id===n)??null,onSelectionChange:e=>r(e.value?.id??null),selectionMode:"single",dataKey:"id",scrollable:!0,scrollHeight:"flex",emptyMessage:o.loading?"Loading…":"No test methods defined.",children:[_jsx(Column,{field:"id",header:"Method ID"}),_jsx(Column,{field:"label",header:"Label"})]})]}),_jsx("section",{className:"tis-editor__detail",children:n&&o.config?.methods[n]?_jsxs(_Fragment,{children:[l&&_jsx("div",{className:"tis-editor__error",children:_jsx("pre",{children:l})}),_jsx(MethodFormEditor,{methodId:n,method:o.config.methods[n],onApply:async e=>{if(n){m(!0);try{await o.putMethod(n,e),d(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}},busy:c,knownAssetTypes:a.types})]}):_jsx("div",{className:"tis-editor__empty",children:"Select a test method on the left, or create a new one."})})]}),_jsxs(Dialog,{header:"New Test Method",visible:h,onHide:()=>u(!1),style:{width:"24rem"},children:[_jsxs("label",{className:"tis-editor__new-method-label",children:["Method ID",_jsx(InputText,{value:_,onChange:e=>g(e.target.value),placeholder:"e.g. translational_traction",autoFocus:!0})]}),_jsx("small",{children:"Canonical key — appears in wire payloads, on-disk paths, and generated code."}),_jsxs("div",{style:{display:"flex",gap:"0.5rem",justifyContent:"flex-end",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:()=>u(!1)}),_jsx(Button,{label:"Create",disabled:!_.trim()||c,onClick:async()=>{const e=_.trim();if(e)if(o.config?.methods[e])d(`A method named "${e}" already exists.`);else{m(!0);try{await o.putMethod(e,EMPTY_METHOD),r(e),u(!1),g("")}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),_jsx(SaveDiffDialog,{visible:f,staged:o.config?.methods??{},invoker:i,onConfirm:async()=>{m(!0);try{await o.save(),p(!1)}catch(e){d(String(e?.message??e))}finally{m(!1)}},onCancel:()=>p(!1)})]})};export default TisConfigEditor;
@@ -1 +1 @@
1
- {"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAa1C,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;CAC1B;AAID,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAoG1D,CAAC"}
1
+ {"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAa1C,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;CAC1B;AAID,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA2J1D,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:a,onCancel:t,onSave:l,siblingNames:r})=>{const[i,o]=useState(blank),[n,s]=useState(null);useEffect(()=>{e&&(o(a?{...a}:{...blank}),s(null))},[e,a]);return _jsxs(Dialog,{header:a?`Edit field: ${a.name}`:"New field",visible:e,onHide:t,style:{width:"36rem"},children:[n&&_jsx("div",{style:{color:"#dc2626",marginBottom:"0.75rem"},children:n}),_jsx(FormRow,{label:"Name",required:!0,hint:"Wire-format key. Also the column name in CSV exports.",children:_jsx(InputText,{value:i.name,onChange:e=>o({...i,name:e.target.value})})}),_jsx(FormRow,{label:"Type",required:!0,children:_jsx(Dropdown,{value:i.type,options:FIELD_TYPES,onChange:e=>o({...i,type:e.value}),editable:!0})}),_jsx(FormRow,{label:"Units",hint:"Display label, appended to form labels (e.g. m/s).",children:_jsx(InputText,{value:i.units??"",onChange:e=>o({...i,units:e.target.value||void 0})})}),_jsx(FormRow,{label:"Label",hint:"Pretty form label. Falls back to name when empty.",children:_jsx(InputText,{value:i.label??"",onChange:e=>o({...i,label:e.target.value||void 0})})}),_jsx(FormRow,{label:"Required",children:_jsx(Checkbox,{checked:!!i.required,onChange:e=>o({...i,required:!!e.checked})})}),_jsx(FormRow,{label:"Source",hint:"Optional gm.* variable to bind this field to.",children:_jsx(InputText,{value:i.source??"",onChange:e=>o({...i,source:e.target.value||void 0}),placeholder:"gm.<variable_name>"})}),_jsx(FormRow,{label:"Scale",hint:"display = raw × scale. Leave blank for 1.0 (no conversion).",children:_jsx(InputNumber,{value:i.scale??null,onValueChange:e=>o({...i,scale:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Description",hint:"Hover tooltip in the form.",children:_jsx(InputTextarea,{rows:2,value:i.description??"",onChange:e=>o({...i,description:e.target.value||void 0})})}),_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:t}),_jsx(Button,{label:"Save",onClick:()=>{const e=(t=i).name.trim()?/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(t.name)?r.some(e=>e===t.name&&e!==a?.name)?`A field named "${t.name}" already exists in this array.`:null:"Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).":"Field name is required.";var t;e?s(e):l(i)}})]})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:a,onCancel:t,onSave:l,siblingNames:n})=>{const[i,r]=useState(blank),[o,m]=useState(""),[s,u]=useState(null);useEffect(()=>{e&&(r(a?{...a}:{...blank}),m(null==a?.default?"":String(a.default)),u(null))},[e,a]);return _jsxs(Dialog,{header:a?`Edit field: ${a.name}`:"New field",visible:e,onHide:t,style:{width:"36rem"},children:[s&&_jsx("div",{style:{color:"#dc2626",marginBottom:"0.75rem"},children:s}),_jsx(FormRow,{label:"Name",required:!0,hint:"Wire-format key. Also the column name in CSV exports.",children:_jsx(InputText,{value:i.name,onChange:e=>r({...i,name:e.target.value})})}),_jsx(FormRow,{label:"Type",required:!0,children:_jsx(Dropdown,{value:i.type,options:FIELD_TYPES,onChange:e=>r({...i,type:e.value}),editable:!0})}),_jsx(FormRow,{label:"Units",hint:"Display label, appended to form labels (e.g. m/s).",children:_jsx(InputText,{value:i.units??"",onChange:e=>r({...i,units:e.target.value||void 0})})}),_jsx(FormRow,{label:"Label",hint:"Pretty form label. Falls back to name when empty.",children:_jsx(InputText,{value:i.label??"",onChange:e=>r({...i,label:e.target.value||void 0})})}),_jsx(FormRow,{label:"Required",children:_jsx(Checkbox,{checked:!!i.required,onChange:e=>r({...i,required:!!e.checked})})}),_jsx(FormRow,{label:"Source",hint:"Optional gm.* variable to bind this field to.",children:_jsx(InputText,{value:i.source??"",onChange:e=>r({...i,source:e.target.value||void 0}),placeholder:"gm.<variable_name>"})}),_jsx(FormRow,{label:"Scale",hint:"display = raw × scale. Leave blank for 1.0 (no conversion).",children:_jsx(InputNumber,{value:i.scale??null,onValueChange:e=>r({...i,scale:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Min",hint:"Optional lower bound the operator can enter (display units). Blank = no minimum.",children:_jsx(InputNumber,{value:i.min??null,onValueChange:e=>r({...i,min:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Max",hint:"Optional upper bound the operator can enter (display units). Blank = no maximum.",children:_jsx(InputNumber,{value:i.max??null,onValueChange:e=>r({...i,max:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Default",hint:"Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.",children:_jsx(InputText,{value:o,onChange:e=>m(e.target.value),placeholder:"e.g. 5 or ${gm.safe_speed}"})}),_jsx(FormRow,{label:"Description",hint:"Hover tooltip in the form.",children:_jsx(InputTextarea,{rows:2,value:i.description??"",onChange:e=>r({...i,description:e.target.value||void 0})})}),_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:t}),_jsx(Button,{label:"Save",onClick:()=>{const e={...i},t=(e=>{const a=e.trim();if(""!==a){if(a.startsWith("${"))return a;if("true"===a)return!0;if("false"===a)return!1;if(/^-?\d*\.?\d+$/.test(a)){const e=Number(a);if(Number.isFinite(e))return e}return a}})(o);void 0===t?delete e.default:e.default=t;const r=(m=e).name.trim()?/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(m.name)?n.some(e=>e===m.name&&e!==a?.name)?`A field named "${m.name}" already exists in this array.`:"number"==typeof m.min&&"number"==typeof m.max&&m.min>m.max?`Min (${m.min}) cannot be greater than Max (${m.max}).`:null:"Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).":"Field name is required.";var m;r?u(r):l(e)}})]})]})};
@@ -16,6 +16,11 @@ export interface TestField {
16
16
  description?: string;
17
17
  default?: unknown;
18
18
  scale?: number;
19
+ /** Optional inclusive bounds for the operator's numeric value, authored
20
+ * in display units (same convention as `default`/`scale`). The form's
21
+ * numeric input rejects values outside `[min, max]`. */
22
+ min?: number;
23
+ max?: number;
19
24
  }
20
25
  export interface ChartAxis {
21
26
  field?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/validation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACG,UAAU,EACxB,MAAM,SAAS,CAAC;AAIjB,MAAM,WAAW,eAAe;IAC5B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,GAAG,eAAe,EAAE,CAmGjF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAOtG"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/validation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACG,UAAU,EACxB,MAAM,SAAS,CAAC;AAIjB,MAAM,WAAW,eAAe;IAC5B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,GAAG,eAAe,EAAE,CAsGjF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAOtG"}
@@ -1 +1 @@
1
- const FIELD_ARRAY_KEYS=["project_fields","config_fields","cycle_fields","results_fields"];export function validateMethod(e,a){const n=[];for(const o of FIELD_ARRAY_KEYS){const t=a[o]??[],s=new Set;t.forEach((a,t)=>{a.name&&""!==a.name.trim()?s.has(a.name)?n.push({path:`${o}.${t}.name`,message:`${e}.${o}: duplicate field name "${a.name}"`}):s.add(a.name):n.push({path:`${o}.${t}.name`,message:`${e}.${o}: empty field name`})})}const o=new Set;for(const e of FIELD_ARRAY_KEYS){(a[e]??[]).forEach(e=>e.name&&o.add(e.name))}const t=new Set(Object.keys(a.raw_data?.columns??{})),s=e=>e.field&&e.field.trim()?{key:e.field,ok:o.has(e.field)}:e.column&&e.column.trim()?{key:e.column,ok:t.has(e.column)}:null,i=a.views??{};for(const[a,o]of Object.entries(i)){if(o?.x){const t=s(o.x);t&&!t.ok&&n.push({path:`views.${a}.x`,message:`${e}.views.${a}: x.field/column "${t.key}" does not match any field or raw_data column`})}(o?.y??[]).forEach((o,t)=>{const i=s(o);i&&!i.ok&&n.push({path:`views.${a}.y.${t}`,message:`${e}.views.${a}.y[${t}]: field/column "${i.key}" does not match any field or raw_data column`})})}const c=a.raw_data;c&&(c.blob_name&&""!==String(c.blob_name).trim()||n.push({path:"raw_data.blob_name",message:`${e}.raw_data: blob_name is empty`}));const m=new Set((a.config_fields??[]).map(e=>e.name).filter(Boolean)),f=a.configurations??[],r=new Set;return f.forEach((a,o)=>{const t="string"==typeof a?.name?a.name.trim():"";t?r.has(t)?n.push({path:`configurations.${o}.name`,message:`${e}.configurations: duplicate configuration name "${t}"`}):r.add(t):n.push({path:`configurations.${o}.name`,message:`${e}.configurations[${o}]: empty configuration name`});const s=a?.defaults??{};for(const a of Object.keys(s))m.has(a)||n.push({path:`configurations.${o}.defaults.${a}`,message:`${e}.configurations.${t||o}: override "${a}" does not match any config_field`})}),n}export function validateMethods(e){const a={};for(const[n,o]of Object.entries(e)){const e=validateMethod(n,o);e.length>0&&(a[n]=e)}return a}
1
+ const FIELD_ARRAY_KEYS=["project_fields","config_fields","cycle_fields","results_fields"];export function validateMethod(e,a){const n=[];for(const t of FIELD_ARRAY_KEYS){const o=a[t]??[],s=new Set;o.forEach((a,o)=>{a.name&&""!==a.name.trim()?s.has(a.name)?n.push({path:`${t}.${o}.name`,message:`${e}.${t}: duplicate field name "${a.name}"`}):s.add(a.name):n.push({path:`${t}.${o}.name`,message:`${e}.${t}: empty field name`}),"number"==typeof a.min&&"number"==typeof a.max&&a.min>a.max&&n.push({path:`${t}.${o}.min`,message:`${e}.${t}.${a.name}: min (${a.min}) is greater than max (${a.max})`})})}const t=new Set;for(const e of FIELD_ARRAY_KEYS){(a[e]??[]).forEach(e=>e.name&&t.add(e.name))}const o=new Set(Object.keys(a.raw_data?.columns??{})),s=e=>e.field&&e.field.trim()?{key:e.field,ok:t.has(e.field)}:e.column&&e.column.trim()?{key:e.column,ok:o.has(e.column)}:null,i=a.views??{};for(const[a,t]of Object.entries(i)){if(t?.x){const o=s(t.x);o&&!o.ok&&n.push({path:`views.${a}.x`,message:`${e}.views.${a}: x.field/column "${o.key}" does not match any field or raw_data column`})}(t?.y??[]).forEach((t,o)=>{const i=s(t);i&&!i.ok&&n.push({path:`views.${a}.y.${o}`,message:`${e}.views.${a}.y[${o}]: field/column "${i.key}" does not match any field or raw_data column`})})}const m=a.raw_data;m&&(m.blob_name&&""!==String(m.blob_name).trim()||n.push({path:"raw_data.blob_name",message:`${e}.raw_data: blob_name is empty`}));const c=new Set((a.config_fields??[]).map(e=>e.name).filter(Boolean)),f=a.configurations??[],r=new Set;return f.forEach((a,t)=>{const o="string"==typeof a?.name?a.name.trim():"";o?r.has(o)?n.push({path:`configurations.${t}.name`,message:`${e}.configurations: duplicate configuration name "${o}"`}):r.add(o):n.push({path:`configurations.${t}.name`,message:`${e}.configurations[${t}]: empty configuration name`});const s=a?.defaults??{};for(const a of Object.keys(s))c.has(a)||n.push({path:`configurations.${t}.defaults.${a}`,message:`${e}.configurations.${o||t}: override "${a}" does not match any config_field`})}),n}export function validateMethods(e){const a={};for(const[n,t]of Object.entries(e)){const e=validateMethod(n,t);e.length>0&&(a[n]=e)}return a}
@@ -1 +1 @@
1
- {"version":3,"file":"useTisConfig.d.ts","sourceRoot":"","sources":["../../src/hooks/useTisConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH;8EAC8E;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;wFACwF;AACxF,MAAM,WAAW,aAAa;IAC1B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QACtC,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,aAAa,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;CACN;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,wBAAgB,YAAY,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC9B,kBAAkB,CAqGpB"}
1
+ {"version":3,"file":"useTisConfig.d.ts","sourceRoot":"","sources":["../../src/hooks/useTisConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH;8EAC8E;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;wFACwF;AACxF,MAAM,WAAW,aAAa;IAC1B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QACtC,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,aAAa,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;CACN;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,wBAAgB,YAAY,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC9B,kBAAkB,CAgHpB"}
@@ -1 +1 @@
1
- import{useCallback,useContext,useEffect,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export function useTisConfig(e,t){const s=useContext(EventEmitterContext),o=t?.invoker??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),[a,r]=useState(null),[i,n]=useState(!0),[c,d]=useState(null),l=useCallback(async()=>{n(!0);try{const t=await o("tis.show_config",{project_id:e});if(!t.success)return d(t.error_message??"tis.show_config failed"),void r(null);const s=t.data??{};r({projectId:e,methods:s.test_methods??{},dirty:!!s.dirty,defaultMethodId:"string"==typeof s.default_method_id?s.default_method_id:""}),d(null)}catch(e){d(String(e?.message??e)),r(null)}finally{n(!1)}},[e,o]);useEffect(()=>{l()},[l]);const u=useCallback(async(t,s)=>{const a=await o("tis.put_method",{project_id:e,method_id:t,method:s});if(!a.success){const e=a.error_message??"tis.put_method failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),f=useCallback(async t=>{const s=await o("tis.remove_method",{project_id:e,method_id:t});if(!s.success){const e=s.error_message??"tis.remove_method failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),m=useCallback(async()=>{const t=await o("tis.save_config",{project_id:e});if(!t.success){const e=t.error_message??"tis.save_config failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),_=useCallback(async()=>{const t=await o("tis.discard_config_changes",{project_id:e});if(!t.success){const e=t.error_message??"tis.discard_config_changes failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]);return{config:a,loading:i,error:c,refresh:l,putMethod:u,removeMethod:f,save:m,revert:_}}
1
+ import{useCallback,useContext,useEffect,useRef,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export function useTisConfig(e,t){const s=useContext(EventEmitterContext),r=t?.invoker??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),o=useRef(r);o.current=r;const[a,n]=useState(null),[i,c]=useState(!0),[u,d]=useState(null),l=useCallback(async()=>{c(!0);try{const t=await o.current("tis.show_config",{project_id:e});if(!t.success)return d(t.error_message??"tis.show_config failed"),void n(null);const s=t.data??{};n({projectId:e,methods:s.test_methods??{},dirty:!!s.dirty,defaultMethodId:"string"==typeof s.default_method_id?s.default_method_id:""}),d(null)}catch(e){d(String(e?.message??e)),n(null)}finally{c(!1)}},[e]);useEffect(()=>{l()},[l]);const f=useCallback(async(t,s)=>{const r=await o.current("tis.put_method",{project_id:e,method_id:t,method:s});if(!r.success){const e=r.error_message??"tis.put_method failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),m=useCallback(async t=>{const s=await o.current("tis.remove_method",{project_id:e,method_id:t});if(!s.success){const e=s.error_message??"tis.remove_method failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),_=useCallback(async()=>{const t=await o.current("tis.save_config",{project_id:e});if(!t.success){const e=t.error_message??"tis.save_config failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),h=useCallback(async()=>{const t=await o.current("tis.discard_config_changes",{project_id:e});if(!t.success){const e=t.error_message??"tis.discard_config_changes failed";throw d(e),new Error(e)}d(null),await l()},[e,l]);return{config:a,loading:i,error:u,refresh:l,putMethod:f,removeMethod:m,save:_,revert:h}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.101",
3
+ "version": "3.3.106",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -60,6 +60,11 @@ export interface TestFieldDef {
60
60
  * Cycle and results values are scaled by the corresponding paths
61
61
  * in TestDataView; the server scales CSV exports too. */
62
62
  scale?: number;
63
+ /** Optional inclusive bounds (display units, same convention as `default`
64
+ * and `scale`) for the operator's numeric entry. The numeric input
65
+ * rejects values outside `[min, max]`; non-numeric fields ignore them. */
66
+ min?: number;
67
+ max?: number;
63
68
  /** Optional fixed set of choices. When present, the field renders
64
69
  * as a dropdown and the operator must pick one of the declared
65
70
  * values rather than typing freely. Each entry is either a bare
@@ -186,6 +191,62 @@ const displayToRaw = (display: any, scale: number | undefined): any => {
186
191
  const hasDescription = (f: TestFieldDef): boolean =>
187
192
  typeof f.description === 'string' && f.description.length > 0;
188
193
 
194
+ /** Matches an FQDN default token like `${gm.safe_speed}` (whole-string). */
195
+ const DEFAULT_TOKEN_RE = /^\$\{\s*([^}]+?)\s*\}$/;
196
+
197
+ /**
198
+ * Resolve a field's `default` to a RAW value for seeding stagedConfig at
199
+ * method-load time. Two forms:
200
+ *
201
+ * - **Literal** (number/string/bool) — authored in DISPLAY units, converted
202
+ * to raw via the field's `scale` (the long-standing convention).
203
+ * - **FQDN token** `${<fqdn>}` — snapshots the *current* value of that tag
204
+ * (read live from the controller, already RAW). Lets an author tie a
205
+ * config_field's load-time default to a value on screen — e.g. a "safe
206
+ * speed" the operator set — so re-loading the method always re-seeds the
207
+ * safe value, while still letting the operator override it afterwards.
208
+ * Distinct from `source`, which is a live two-way binding; a token default
209
+ * is a one-time seed.
210
+ *
211
+ * Returns `{ raw, ok }`. `ok === false` means an `${fqdn}` token whose tag is
212
+ * unknown or has no value yet — the caller skips seeding that field rather
213
+ * than writing garbage.
214
+ */
215
+ const resolveDefaultRaw = (
216
+ field: TestFieldDef,
217
+ findTagByFqdn: (fqdn: string) => { tagName: string } | undefined,
218
+ rawValues: Record<string, unknown>,
219
+ ): { raw: any; ok: boolean } => {
220
+ const d = field.default;
221
+ if (typeof d === 'string') {
222
+ const m = d.match(DEFAULT_TOKEN_RE);
223
+ if (m) {
224
+ const tag = findTagByFqdn(m[1]);
225
+ const v = tag ? rawValues[tag.tagName] : undefined;
226
+ if (v === undefined || v === null) return { raw: undefined, ok: false };
227
+ return { raw: v, ok: true }; // tag value is already RAW
228
+ }
229
+ }
230
+ return { raw: displayToRaw(d, field.scale), ok: true };
231
+ };
232
+
233
+ /**
234
+ * Range check for a numeric field's stored RAW value against its declared
235
+ * `min`/`max` (authored in DISPLAY units). Returns a short human-readable
236
+ * reason ("must be ≥ 5") when out of range, or null when in range / not
237
+ * applicable (empty value, no bounds, or non-numeric). Storage is raw, so we
238
+ * convert to display first to compare against the author's display-unit bounds.
239
+ */
240
+ const rangeIssue = (f: TestFieldDef, raw: any): string | null => {
241
+ if (raw === undefined || raw === '' || raw === null) return null;
242
+ if (f.min == null && f.max == null) return null;
243
+ const disp = Number(rawToDisplay(Number(raw), f.scale));
244
+ if (!Number.isFinite(disp)) return null;
245
+ if (f.min != null && disp < f.min) return `must be ≥ ${f.min}`;
246
+ if (f.max != null && disp > f.max) return `must be ≤ ${f.max}`;
247
+ return null;
248
+ };
249
+
189
250
  /**
190
251
  * Normalise a field's `options` (bare scalars and/or `{label, value}`
191
252
  * pairs) into the `{ label, value }[]` shape PrimeReact's Dropdown
@@ -401,13 +462,19 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
401
462
  for (const field of schema.config_fields) {
402
463
  if (field.name === 'sample_id') continue;
403
464
  if (field.default === undefined || field.default === null) continue;
465
+ // Resolve the default to a RAW value. Literals are authored in
466
+ // DISPLAY units (converted via `scale`); an `${fqdn}` token
467
+ // snapshots that tag's current (already-raw) value so a
468
+ // load-time default can track a value on screen. A token that
469
+ // can't resolve (tag unknown / not yet read) leaves the field
470
+ // unset rather than seeding garbage.
471
+ const { raw: rawDefault, ok } = resolveDefaultRaw(field, findTagByFqdn, rawValues);
472
+ if (!ok) {
473
+ console.warn(
474
+ `[TestSetupForm] default token ${String(field.default)} for "${field.name}" did not resolve; leaving it unset.`);
475
+ continue;
476
+ }
404
477
  if (next === prev) next = { ...prev };
405
- // Schema defaults are authored in DISPLAY units (per
406
- // the agreed convention) so the value the author reads
407
- // in project.json matches the field's `units` label.
408
- // Convert to raw before storing in stagedConfig / GM
409
- // so the rest of the pipeline sees the canonical value.
410
- const rawDefault = displayToRaw(field.default, field.scale);
411
478
  next[field.name] = rawDefault;
412
479
  if (field.source) {
413
480
  // Mirror handleFieldChange: write to GM so the
@@ -430,7 +497,7 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
430
497
  });
431
498
 
432
499
  tis.setConfigurationName(firstConfig ? firstConfig.name : '');
433
- }, [schema, methodId, write, tis.defaultsAppliedForMethod, tis.markDefaultsAppliedForMethod, tis.setConfigurationName]);
500
+ }, [schema, methodId, write, rawValues, findTagByFqdn, tis.defaultsAppliedForMethod, tis.markDefaultsAppliedForMethod, tis.setConfigurationName]);
434
501
 
435
502
  // Seed and live-update config_fields that declare a `source`.
436
503
  useEffect(() => {
@@ -467,10 +534,10 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
467
534
 
468
535
  for (const field of schema.config_fields) {
469
536
  if (field.name === 'sample_id') continue;
470
- if (field.required) {
471
- const v = config[field.name];
472
- if (v === undefined || v === '' || v === null) { valid = false; break; }
473
- }
537
+ const v = config[field.name];
538
+ const empty = v === undefined || v === '' || v === null;
539
+ if (field.required && empty) { valid = false; break; }
540
+ if (rangeIssue(field, v)) { valid = false; break; }
474
541
  }
475
542
 
476
543
  setIsValid(valid);
@@ -501,9 +568,11 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
501
568
  ]);
502
569
 
503
570
  const isFieldValid = (field: TestFieldDef) => {
504
- if (!field.required) return true;
505
571
  const v = config[field.name];
506
- return v !== undefined && v !== '' && v !== null;
572
+ const empty = v === undefined || v === '' || v === null;
573
+ if (field.required && empty) return false;
574
+ if (rangeIssue(field, v)) return false;
575
+ return true;
507
576
  };
508
577
 
509
578
  const handleSampleIdChange = (value: string) => {
@@ -595,6 +664,11 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
595
664
  value={config[field.name] != null
596
665
  ? Number(rawToDisplay(Number(config[field.name]), field.scale))
597
666
  : null}
667
+ // min/max are authored in display units, matching the
668
+ // value rendered above — ValueInput rejects out-of-range
669
+ // entries on accept.
670
+ min={field.min}
671
+ max={field.max}
598
672
  onValueChanged={(val) => handleFieldChange(field, val)}
599
673
  className={!valid ? 'p-invalid' : ''}
600
674
  />
@@ -682,11 +756,14 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
682
756
  if (schema) {
683
757
  for (const field of schema.config_fields) {
684
758
  if (field.name === 'sample_id') continue;
685
- if (!field.required) continue;
686
759
  const v = config[field.name];
687
- if (v === undefined || v === '' || v === null) {
760
+ const empty = v === undefined || v === '' || v === null;
761
+ if (field.required && empty) {
688
762
  issues.push(`Required field "${labelOf(field)}" is empty.`);
763
+ continue;
689
764
  }
765
+ const re = rangeIssue(field, v);
766
+ if (re) issues.push(`"${labelOf(field)}" ${re}.`);
690
767
  }
691
768
  }
692
769
  return issues;
@@ -58,10 +58,15 @@ const EMPTY_METHOD: TestMethod = {
58
58
 
59
59
  export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, invoker }) => {
60
60
  const ctx = useContext(EventEmitterContext);
61
- // Resolve invoker once so it can be passed into the SaveDiffDialog
62
- // alongside the hook's internal use of it.
63
- const effectiveInvoker: TisIpcInvoker = invoker
64
- ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
61
+ // Resolve the invoker ONCE and memoize it so it keeps a stable identity
62
+ // across renders. Both useTisConfig and SaveDiffDialog key effects off
63
+ // this invoker; an invoker rebuilt every render drove a refetch loop in
64
+ // the hook that continuously reset the editor draft and wiped edits.
65
+ const effectiveInvoker: TisIpcInvoker = useMemo(
66
+ () => invoker
67
+ ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any)),
68
+ [invoker, ctx],
69
+ );
65
70
 
66
71
  const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
67
72
  const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
@@ -35,15 +35,34 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
35
35
  visible, initial, onCancel, onSave, siblingNames,
36
36
  }) => {
37
37
  const [draft, setDraft] = useState<TestField>(blank);
38
+ // `default` is edited as free text so a numeric literal, a string, or an
39
+ // `${fqdn}` token can all be entered in one field; it's coerced on save.
40
+ const [defaultText, setDefaultText] = useState<string>('');
38
41
  const [error, setError] = useState<string | null>(null);
39
42
 
40
43
  useEffect(() => {
41
44
  if (visible) {
42
45
  setDraft(initial ? { ...initial } : { ...blank });
46
+ setDefaultText(initial?.default == null ? '' : String(initial.default));
43
47
  setError(null);
44
48
  }
45
49
  }, [visible, initial]);
46
50
 
51
+ // Turn the free-text Default into its stored form: an `${fqdn}` token and
52
+ // other non-numerics stay strings; numeric/bool literals are stored typed.
53
+ const coerceDefault = (s: string): unknown => {
54
+ const t = s.trim();
55
+ if (t === '') return undefined;
56
+ if (t.startsWith('${')) return t;
57
+ if (t === 'true') return true;
58
+ if (t === 'false') return false;
59
+ if (/^-?\d*\.?\d+$/.test(t)) {
60
+ const n = Number(t);
61
+ if (Number.isFinite(n)) return n;
62
+ }
63
+ return t;
64
+ };
65
+
47
66
  const validate = (f: TestField): string | null => {
48
67
  if (!f.name.trim()) return 'Field name is required.';
49
68
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(f.name)) {
@@ -51,13 +70,20 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
51
70
  }
52
71
  const dupOfOther = siblingNames.some(n => n === f.name && n !== initial?.name);
53
72
  if (dupOfOther) return `A field named "${f.name}" already exists in this array.`;
73
+ if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
74
+ return `Min (${f.min}) cannot be greater than Max (${f.max}).`;
75
+ }
54
76
  return null;
55
77
  };
56
78
 
57
79
  const handleSave = () => {
58
- const err = validate(draft);
80
+ const candidate: TestField = { ...draft };
81
+ const dv = coerceDefault(defaultText);
82
+ if (dv === undefined) delete (candidate as { default?: unknown }).default;
83
+ else candidate.default = dv;
84
+ const err = validate(candidate);
59
85
  if (err) { setError(err); return; }
60
- onSave(draft);
86
+ onSave(candidate);
61
87
  };
62
88
 
63
89
  return (
@@ -118,6 +144,35 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
118
144
  maxFractionDigits={9}
119
145
  />
120
146
  </FormRow>
147
+ <FormRow label="Min" hint="Optional lower bound the operator can enter (display units). Blank = no minimum.">
148
+ <InputNumber
149
+ value={draft.min ?? null}
150
+ onValueChange={(e) =>
151
+ setDraft({ ...draft, min: typeof e.value === 'number' ? e.value : undefined })
152
+ }
153
+ mode="decimal"
154
+ minFractionDigits={0}
155
+ maxFractionDigits={9}
156
+ />
157
+ </FormRow>
158
+ <FormRow label="Max" hint="Optional upper bound the operator can enter (display units). Blank = no maximum.">
159
+ <InputNumber
160
+ value={draft.max ?? null}
161
+ onValueChange={(e) =>
162
+ setDraft({ ...draft, max: typeof e.value === 'number' ? e.value : undefined })
163
+ }
164
+ mode="decimal"
165
+ minFractionDigits={0}
166
+ maxFractionDigits={9}
167
+ />
168
+ </FormRow>
169
+ <FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
170
+ <InputText
171
+ value={defaultText}
172
+ onChange={(e) => setDefaultText(e.target.value)}
173
+ placeholder="e.g. 5 or ${gm.safe_speed}"
174
+ />
175
+ </FormRow>
121
176
  <FormRow label="Description" hint="Hover tooltip in the form.">
122
177
  <InputTextarea
123
178
  rows={2}
@@ -22,6 +22,11 @@ export interface TestField {
22
22
  description?: string;
23
23
  default?: unknown;
24
24
  scale?: number;
25
+ /** Optional inclusive bounds for the operator's numeric value, authored
26
+ * in display units (same convention as `default`/`scale`). The form's
27
+ * numeric input rejects values outside `[min, max]`. */
28
+ min?: number;
29
+ max?: number;
25
30
  }
26
31
 
27
32
  export interface ChartAxis {
@@ -33,6 +33,9 @@ export function validateMethod(methodId: string, m: TestMethod): ValidationError
33
33
  } else {
34
34
  seen.add(f.name);
35
35
  }
36
+ if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
37
+ errs.push({ path: `${key}.${i}.min`, message: `${methodId}.${key}.${f.name}: min (${f.min}) is greater than max (${f.max})` });
38
+ }
36
39
  });
37
40
  }
38
41
 
@@ -16,7 +16,7 @@
16
16
  * demo and by tests). When omitted, the hook reads `EventEmitterContext`.
17
17
  */
18
18
 
19
- import { useCallback, useContext, useEffect, useState } from 'react';
19
+ import { useCallback, useContext, useEffect, useRef, useState } from 'react';
20
20
  import { EventEmitterContext } from '../core/EventEmitterContext';
21
21
  import { MessageType } from '../hub/CommandMessage';
22
22
 
@@ -70,6 +70,18 @@ export function useTisConfig(
70
70
  return await ctx.invoke(topic as any, MessageType.Request, payload as any);
71
71
  });
72
72
 
73
+ // Hold the invoker in a ref so the memoized callbacks below keep a STABLE
74
+ // identity even when the caller passes a freshly-built invoker on every
75
+ // render (the common case — TisConfigEditor does exactly that). Depending
76
+ // on `invoker` directly made `refresh` change every render, so the mount
77
+ // effect re-ran in an infinite refetch loop: each refetch replaced
78
+ // `config`, which reset MethodFormEditor's draft, silently wiping
79
+ // in-progress edits (deleted views reappeared, Apply never stayed
80
+ // enabled). The ref is refreshed each render so calls still use the latest
81
+ // invoker without destabilising the callbacks.
82
+ const invokerRef = useRef(invoker);
83
+ invokerRef.current = invoker;
84
+
73
85
  const [config, setConfig] = useState<TisConfig | null>(null);
74
86
  const [loading, setLoading] = useState<boolean>(true);
75
87
  const [error, setError] = useState<string | null>(null);
@@ -80,7 +92,7 @@ export function useTisConfig(
80
92
  const refresh = useCallback(async () => {
81
93
  setLoading(true);
82
94
  try {
83
- const resp = await invoker('tis.show_config', { project_id: projectId });
95
+ const resp = await invokerRef.current('tis.show_config', { project_id: projectId });
84
96
  if (!resp.success) {
85
97
  setError(resp.error_message ?? 'tis.show_config failed');
86
98
  setConfig(null);
@@ -102,15 +114,14 @@ export function useTisConfig(
102
114
  } finally {
103
115
  setLoading(false);
104
116
  }
105
- // eslint-disable-next-line react-hooks/exhaustive-deps
106
- }, [projectId, invoker]);
117
+ }, [projectId]);
107
118
 
108
119
  useEffect(() => {
109
120
  void refresh();
110
121
  }, [refresh]);
111
122
 
112
123
  const putMethod = useCallback(async (methodId: string, method: TestMethod) => {
113
- const resp = await invoker('tis.put_method', {
124
+ const resp = await invokerRef.current('tis.put_method', {
114
125
  project_id: projectId,
115
126
  method_id: methodId,
116
127
  method,
@@ -122,10 +133,10 @@ export function useTisConfig(
122
133
  }
123
134
  setError(null);
124
135
  await refresh();
125
- }, [invoker, projectId, refresh]);
136
+ }, [projectId, refresh]);
126
137
 
127
138
  const removeMethod = useCallback(async (methodId: string) => {
128
- const resp = await invoker('tis.remove_method', {
139
+ const resp = await invokerRef.current('tis.remove_method', {
129
140
  project_id: projectId,
130
141
  method_id: methodId,
131
142
  });
@@ -136,10 +147,10 @@ export function useTisConfig(
136
147
  }
137
148
  setError(null);
138
149
  await refresh();
139
- }, [invoker, projectId, refresh]);
150
+ }, [projectId, refresh]);
140
151
 
141
152
  const save = useCallback(async () => {
142
- const resp = await invoker('tis.save_config', { project_id: projectId });
153
+ const resp = await invokerRef.current('tis.save_config', { project_id: projectId });
143
154
  if (!resp.success) {
144
155
  const m = resp.error_message ?? 'tis.save_config failed';
145
156
  setError(m);
@@ -147,10 +158,10 @@ export function useTisConfig(
147
158
  }
148
159
  setError(null);
149
160
  await refresh();
150
- }, [invoker, projectId, refresh]);
161
+ }, [projectId, refresh]);
151
162
 
152
163
  const revert = useCallback(async () => {
153
- const resp = await invoker('tis.discard_config_changes', { project_id: projectId });
164
+ const resp = await invokerRef.current('tis.discard_config_changes', { project_id: projectId });
154
165
  if (!resp.success) {
155
166
  const m = resp.error_message ?? 'tis.discard_config_changes failed';
156
167
  setError(m);
@@ -158,7 +169,7 @@ export function useTisConfig(
158
169
  }
159
170
  setError(null);
160
171
  await refresh();
161
- }, [invoker, projectId, refresh]);
172
+ }, [projectId, refresh]);
162
173
 
163
174
  return { config, loading, error, refresh, putMethod, removeMethod, save, revert };
164
175
  }