@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.
- package/dist/components/tis/TestSetupForm.d.ts +5 -0
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/types.d.ts +5 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/dist/hooks/useTisConfig.d.ts.map +1 -1
- package/dist/hooks/useTisConfig.js +1 -1
- package/package.json +1 -1
- package/src/components/tis/TestSetupForm.tsx +92 -15
- package/src/components/tis-editor/TisConfigEditor.tsx +9 -4
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +57 -2
- package/src/components/tis-editor/types.ts +5 -0
- package/src/components/tis-editor/validation.ts +3 -0
- package/src/hooks/useTisConfig.ts +23 -12
|
@@ -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;
|
|
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,
|
|
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,
|
|
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:
|
|
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":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,SAAS,GACf,QAAQ,GACR,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAC7B,KAAK,GAAG,KAAK,GACb,MAAM,CAAC;AAEb,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,SAAS,GACf,QAAQ,GACR,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAC7B,KAAK,GAAG,KAAK,GACb,MAAM,CAAC;AAEb,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;6DAEyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG,WAAW,CAAC;AAE1D,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7B,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,SAAS;IACtB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,aAAa,CAAC;AAC3D,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,eAAe,GAAG,cAAc,GAAG,gBAAgB,CAAC"}
|
|
@@ -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,
|
|
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
|
|
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,
|
|
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),
|
|
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
|
@@ -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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
}, [
|
|
136
|
+
}, [projectId, refresh]);
|
|
126
137
|
|
|
127
138
|
const removeMethod = useCallback(async (methodId: string) => {
|
|
128
|
-
const resp = await
|
|
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
|
-
}, [
|
|
150
|
+
}, [projectId, refresh]);
|
|
140
151
|
|
|
141
152
|
const save = useCallback(async () => {
|
|
142
|
-
const resp = await
|
|
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
|
-
}, [
|
|
161
|
+
}, [projectId, refresh]);
|
|
151
162
|
|
|
152
163
|
const revert = useCallback(async () => {
|
|
153
|
-
const resp = await
|
|
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
|
-
}, [
|
|
172
|
+
}, [projectId, refresh]);
|
|
162
173
|
|
|
163
174
|
return { config, loading, error, refresh, putMethod, removeMethod, save, revert };
|
|
164
175
|
}
|