@adcops/autocore-react 3.3.109 → 3.3.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/assets/JogXLeft.d.ts +4 -0
  2. package/dist/assets/JogXLeft.d.ts.map +1 -0
  3. package/dist/assets/JogXLeft.js +1 -0
  4. package/dist/assets/JogXRight.d.ts +4 -0
  5. package/dist/assets/JogXRight.d.ts.map +1 -0
  6. package/dist/assets/JogXRight.js +1 -0
  7. package/dist/assets/JogYDownLeft.d.ts +4 -0
  8. package/dist/assets/JogYDownLeft.d.ts.map +1 -0
  9. package/dist/assets/JogYDownLeft.js +1 -0
  10. package/dist/assets/JogYUpRight.d.ts +4 -0
  11. package/dist/assets/JogYUpRight.d.ts.map +1 -0
  12. package/dist/assets/JogYUpRight.js +1 -0
  13. package/dist/assets/index.d.ts +4 -4
  14. package/dist/assets/index.d.ts.map +1 -1
  15. package/dist/assets/index.js +1 -1
  16. package/dist/components/JogPanel.d.ts +29 -4
  17. package/dist/components/JogPanel.d.ts.map +1 -1
  18. package/dist/components/JogPanel.js +1 -1
  19. package/dist/components/ValueInput.d.ts.map +1 -1
  20. package/dist/components/ValueInput.js +1 -1
  21. package/dist/components/network/StagedChangeBanner.d.ts.map +1 -1
  22. package/dist/components/network/StagedChangeBanner.js +1 -1
  23. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  24. package/dist/components/tis/ResultHistoryTable.js +1 -1
  25. package/dist/components/tis/ScienceTable.css +87 -0
  26. package/dist/components/tis/ScienceTable.d.ts +35 -0
  27. package/dist/components/tis/ScienceTable.d.ts.map +1 -0
  28. package/dist/components/tis/ScienceTable.js +1 -0
  29. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  30. package/dist/components/tis/TestDataView.js +1 -1
  31. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  32. package/dist/components/tis/TestSetupForm.js +1 -1
  33. package/package.json +1 -1
  34. package/src/assets/{JogXNeg.tsx → JogXLeft.tsx} +6 -3
  35. package/src/assets/{JogXPos.tsx → JogXRight.tsx} +6 -3
  36. package/src/assets/{JogYPos.tsx → JogYDownLeft.tsx} +2 -2
  37. package/src/assets/{JogYNeg.tsx → JogYUpRight.tsx} +3 -2
  38. package/src/assets/index.ts +4 -4
  39. package/src/components/JogPanel.tsx +64 -59
  40. package/src/components/ValueInput.tsx +10 -3
  41. package/src/components/network/StagedChangeBanner.tsx +22 -3
  42. package/src/components/tis/ResultHistoryTable.tsx +19 -8
  43. package/src/components/tis/ScienceTable.css +87 -0
  44. package/src/components/tis/ScienceTable.tsx +114 -0
  45. package/src/components/tis/TestDataView.tsx +49 -54
  46. package/src/components/tis/TestSetupForm.tsx +6 -2
  47. package/dist/assets/JogXNeg.d.ts +0 -4
  48. package/dist/assets/JogXNeg.d.ts.map +0 -1
  49. package/dist/assets/JogXNeg.js +0 -1
  50. package/dist/assets/JogXPos.d.ts +0 -4
  51. package/dist/assets/JogXPos.d.ts.map +0 -1
  52. package/dist/assets/JogXPos.js +0 -1
  53. package/dist/assets/JogYNeg.d.ts +0 -4
  54. package/dist/assets/JogYNeg.d.ts.map +0 -1
  55. package/dist/assets/JogYNeg.js +0 -1
  56. package/dist/assets/JogYPos.d.ts +0 -4
  57. package/dist/assets/JogYPos.d.ts.map +0 -1
  58. package/dist/assets/JogYPos.js +0 -1
@@ -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,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,v]=useState(!1),[y,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;if(e.source){const t=l(e.source),s=t?r[t.tagName]:void 0;if(null!=s)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 v(!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(v(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:y,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
+ 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]),n=i?s[i.tagName]:void 0;return null==n?{raw:void 0,ok:!1}:{raw:n,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));if(!Number.isFinite(s))return null;const i=null!=e.min&&null!=e.max?Math.min(e.min,e.max):e.min,n=null!=e.min&&null!=e.max?Math.max(e.min,e.max):e.max;return null!=i&&s<i?`must be ≥ ${i}`:null!=n&&s>n?`must be ≤ ${n}`: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 n=useAmsAssets(),a=useAmsRoles(),o=useMemo(()=>n.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=a[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}}),[n,a,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 n=useTis(),{invoke:a,write:o}=useContext(EventEmitterContext),{rawValues:l,findTagByFqdn:r}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(n.schemas),[n.schemas]),d=n.selection.projectId,m=""!==d.trim()&&n.projectKnown(d.trim()),[u,p]=useState(n.selection.methodId||t||n.defaultMethodId||""),[f,h]=useState(n.selection.sampleId||""),g=n.stagedConfig,x=e=>{const t="function"==typeof e?e(n.stagedConfig):e;t!==n.stagedConfig&&n.setStagedConfig(t)},_=e??(u?n.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),n=displayToRaw(i,e?.scale);t[s]=n,e?.source&&Promise.resolve().then(()=>o(e.source,n)).catch(e=>{})}return t};useEffect(()=>{n.selection.methodId!==u&&u&&n.setSelection({methodId:u}),s&&s(u)},[u]),useEffect(()=>{n.selection.sampleId!==f&&n.setSelection({sampleId:f})},[f]),useEffect(()=>{n.state.stagedSampleId&&n.state.stagedSampleId!==f&&h(n.state.stagedSampleId)},[n.state.stagedSampleId]),useEffect(()=>{n.selection.methodId&&n.selection.methodId!==u&&p(n.selection.methodId)},[n.selection.methodId]);const[b,v]=useState(!1),[y,N]=useState(!1),[C,I]=useState(!1),T=_?.configurations??[],S=T.find(e=>e.name===n.configurationName),[w,k]=useState({open:!1,title:"",body:null});useEffect(()=>{if(!_||!u)return;if(n.defaultsAppliedForMethod===u)return;n.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;if(e.source){const t=r(e.source),s=t?l[t.tagName]:void 0;if(null!=s)continue}const{raw:i,ok:n}=resolveDefaultRaw(e,r,l);n&&(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}),n.setConfigurationName(e?e.name:"")},[_,u,o,l,r,n.defaultsAppliedForMethod,n.markDefaultsAppliedForMethod,n.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=r(s.source);if(!i)continue;const n=l[i.tagName];null!=n&&(t[s.name]!==n&&(t===e&&(t={...e}),t[s.name]=n))}return t})},[_,l,r]),useEffect(()=>{if(!_)return void v(!1);let e=!0;m||(e=!1),u.trim()||(e=!1),f.trim()||(e=!1),e&&!n.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(v(e),i&&i(e,g),e){const{sample_id:e,...t}=g??{},s={...n.projectFields,...t};a("tis.stage_test",MessageType.Request,{project_id:d,method_id:u,sample_id:f,config:s}).catch(e=>{})}},[g,_,d,u,f,m,n.projectFields,n.projectFieldsLoaded,i,a]);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:n.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&&!n.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."),n.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 n=rangeIssue(t,s);n&&e.push(`"${labelOf(t)}" ${n}.`)}return e})(),t=n.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=n.projectAssetRefs??[],i=`config.${e.name}`,a=t.find(e=>"by_id_field"===e.select&&e.from===i);if(a)return a.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,a=!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}):a?_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:y,onHide:()=>N(!1),currentMethodId:u,onSelected:e=>p(e)}),_jsx(ConfigurationDialog,{visible:C,onHide:()=>I(!1),configurations:T,currentConfigName:n.configurationName,onSelected:e=>{const t=T.find(t=>t.name===e);t&&(x(e=>j(t,{...e})),n.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})]})};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.109",
3
+ "version": "3.3.112",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -3,11 +3,14 @@
3
3
  * Created Date: 2026-06-02
4
4
  * -----
5
5
  *
6
+ * Named for the direction it points (left), not a jog polarity — which
7
+ * axis end "positive" corresponds to varies by machine, so the icon is
8
+ * chosen by direction via the JogPanel `icon` field, decoupled from action.
6
9
  */
7
10
 
8
11
  import type { SVGProps } from "react";
9
12
 
10
- export const SvgJogXNeg = (props: SVGProps<SVGSVGElement>) => (
13
+ export const SvgJogXLeft = (props: SVGProps<SVGSVGElement>) => (
11
14
  <svg
12
15
  xmlns="http://www.w3.org/2000/svg"
13
16
  width="24"
@@ -20,11 +23,11 @@ export const SvgJogXNeg = (props: SVGProps<SVGSVGElement>) => (
20
23
  strokeLinejoin="round"
21
24
  {...props}
22
25
  >
23
- {/* Triangle pointing left (X negative) */}
26
+ {/* Triangle pointing left */}
24
27
  <path d="M10 6 3 12 10 18Z" fill="currentColor" />
25
28
  {/* Letter X */}
26
29
  <path d="M14 6l8 12M22 6l-8 12" />
27
30
  </svg>
28
31
  );
29
32
 
30
- export default SvgJogXNeg;
33
+ export default SvgJogXLeft;
@@ -3,11 +3,14 @@
3
3
  * Created Date: 2026-06-02
4
4
  * -----
5
5
  *
6
+ * Named for the direction it points (right), not a jog polarity — which
7
+ * axis end "positive" corresponds to varies by machine, so the icon is
8
+ * chosen by direction via the JogPanel `icon` field, decoupled from action.
6
9
  */
7
10
 
8
11
  import type { SVGProps } from "react";
9
12
 
10
- export const SvgJogXPos = (props: SVGProps<SVGSVGElement>) => (
13
+ export const SvgJogXRight = (props: SVGProps<SVGSVGElement>) => (
11
14
  <svg
12
15
  xmlns="http://www.w3.org/2000/svg"
13
16
  width="24"
@@ -22,9 +25,9 @@ export const SvgJogXPos = (props: SVGProps<SVGSVGElement>) => (
22
25
  >
23
26
  {/* Letter X */}
24
27
  <path d="M2 6l8 12M10 6l-8 12" />
25
- {/* Triangle pointing right (X positive) */}
28
+ {/* Triangle pointing right */}
26
29
  <path d="M14 6 21 12 14 18Z" fill="currentColor" />
27
30
  </svg>
28
31
  );
29
32
 
30
- export default SvgJogXPos;
33
+ export default SvgJogXRight;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { SVGProps } from "react";
9
9
 
10
- export const SvgJogYNeg = (props: SVGProps<SVGSVGElement>) => (
10
+ export const SvgJogYDownLeft = (props: SVGProps<SVGSVGElement>) => (
11
11
  <svg
12
12
  xmlns="http://www.w3.org/2000/svg"
13
13
  width="24"
@@ -27,4 +27,4 @@ export const SvgJogYNeg = (props: SVGProps<SVGSVGElement>) => (
27
27
  </svg>
28
28
  );
29
29
 
30
- export default SvgJogYNeg;
30
+ export default SvgJogYDownLeft;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { SVGProps } from "react";
9
9
 
10
- export const SvgJogYPos = (props: SVGProps<SVGSVGElement>) => (
10
+ export const SvgJogYUpRight = (props: SVGProps<SVGSVGElement>) => (
11
11
  <svg
12
12
  xmlns="http://www.w3.org/2000/svg"
13
13
  width="24"
@@ -27,4 +27,5 @@ export const SvgJogYPos = (props: SVGProps<SVGSVGElement>) => (
27
27
  </svg>
28
28
  );
29
29
 
30
- export default SvgJogYPos;
30
+ export default SvgJogYUpRight;
31
+
@@ -3,10 +3,10 @@ export { default as Distance } from "./Distance";
3
3
  export { default as JogLong } from "./JogLong";
4
4
  export { default as JogMedium } from "./JogMedium";
5
5
  export { default as JogShort } from "./JogShort";
6
- export { default as JogXPos } from "./JogXPos";
7
- export { default as JogXNeg } from "./JogXNeg";
8
- export { default as JogYPos } from "./JogYPos";
9
- export { default as JogYNeg } from "./JogYNeg";
6
+ export { default as JogXRight } from "./JogXRight";
7
+ export { default as JogXLeft } from "./JogXLeft";
8
+ export { default as JogYUpRight } from "./JogYUpRight";
9
+ export { default as JogYDownLeft } from "./JogYDownLeft";
10
10
  export { default as JogZPos } from "./JogZPos";
11
11
  export { default as JogZNeg } from "./JogZNeg";
12
12
  export { default as On } from "./On";
@@ -19,7 +19,7 @@ import { Distance, JogShort, JogMedium, JogLong } from '../assets';
19
19
  import { Speed, SpeedSlow, SpeedMedium, SpeedFast } from '../assets';
20
20
  import { RotationCcwA, RotationCcwB, RotationCcwC } from '../assets';
21
21
  import { RotationCwA, RotationCwB, RotationCwC } from '../assets';
22
- import { JogXPos, JogXNeg, JogYPos, JogYNeg, JogZPos, JogZNeg } from '../assets';
22
+ import { JogXLeft, JogXRight, JogYDownLeft, JogYUpRight, JogZPos, JogZNeg } from '../assets';
23
23
 
24
24
  /**
25
25
  * Enumerates the source button or action when an event occurs.
@@ -80,6 +80,17 @@ export enum JogSpeedAction {
80
80
  * Jog button definition type for defining the button in the JogPanel.
81
81
  */
82
82
  export interface JogPanelButtonDefinition {
83
+ /**
84
+ * Direction key selecting the arrow icon shown on the button, e.g.
85
+ * `jog_x_left`, `jog_x_right`, `jog_y_up_right`, `jog_y_down_left`,
86
+ * `jog_z_up`, `jog_z_down`, or the rotation keys `rot_{a|b|c}_{cw|ccw}`.
87
+ *
88
+ * Deliberately independent of `action`: which physical direction a
89
+ * machine's "jog X positive" moves is not consistent across machines,
90
+ * so the icon (what the operator sees) is chosen here by direction while
91
+ * `action` carries the polarity sent to the control program. See
92
+ * `selectIcon`. Legacy `pi pi-arrow-*` values are still accepted.
93
+ */
83
94
  icon: string;
84
95
  alt: string;
85
96
  action: JogPanelAction
@@ -126,18 +137,18 @@ interface JogPanelState {
126
137
  * A default jog button configuration for linear 3D motion.
127
138
  */
128
139
  export const DefaultLinearJogButtons : (JogPanelButtonDefinition | undefined)[][] = [
129
- [undefined, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "pi pi-arrow-down-left", action: JogPanelAction.yNegative, alt: "Y Negative" }],
130
- [{ icon: "pi pi-arrow-left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "pi pi-arrow-right", action: JogPanelAction.xPositive, alt: "X Positive" }],
131
- [{ icon: "pi pi-arrow-up-right", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
140
+ [undefined, { icon: "jog_z_up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "jog_y_up_right", action: JogPanelAction.yNegative, alt: "Y Negative" }],
141
+ [{ icon: "jog_x_left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "jog_x_right", action: JogPanelAction.xPositive, alt: "X Positive" }],
142
+ [{ icon: "jog_y_down_left", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "jog_z_down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
132
143
  ];
133
144
 
134
145
  /**
135
146
  * A default jog button configuration for rotational 3D motion.
136
147
  */
137
148
  export const DefaultRotationJogButtons : (JogPanelButtonDefinition | undefined)[][] = [
138
- [{ icon: "pi", action: JogPanelAction.bPositive, alt: "Rot B Positive" }, { icon: "pi", action: JogPanelAction.cPositive, alt: "Rot C Positive" }, undefined],
139
- [{ icon: "pi", action: JogPanelAction.aNegative, alt: "Rot A Negative" }, undefined, { icon: "pi", action: JogPanelAction.aPositive, alt: "Rot A Positive" }],
140
- [undefined, { icon: "pi", action: JogPanelAction.cNegative, alt: "Rot C Negative" }, { icon: "pi ", action: JogPanelAction.bNegative, alt: "Rot B Negative" }],
149
+ [{ icon: "rot_b_cw", action: JogPanelAction.bPositive, alt: "Rot B Positive" }, { icon: "rot_c_cw", action: JogPanelAction.cPositive, alt: "Rot C Positive" }, undefined],
150
+ [{ icon: "rot_a_ccw", action: JogPanelAction.aNegative, alt: "Rot A Negative" }, undefined, { icon: "rot_a_cw", action: JogPanelAction.aPositive, alt: "Rot A Positive" }],
151
+ [undefined, { icon: "rot_c_ccw", action: JogPanelAction.cNegative, alt: "Rot C Negative" }, { icon: "rot_b_ccw", action: JogPanelAction.bNegative, alt: "Rot B Negative" }],
141
152
  ];
142
153
 
143
154
  /**
@@ -157,10 +168,14 @@ export const DefaultRotationJogButtons : (JogPanelButtonDefinition | undefined)[
157
168
  * Example:
158
169
  * ```
159
170
  * export const kDefaultButtonDefinitions : (JogPanelButtonDefinition | undefined)[][] = [
160
- * [undefined, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "pi pi-arrow-down-left", action: JogPanelAction.yNegative, alt: "Y Negative" }],
161
- * [{ icon: "pi pi-arrow-left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "pi pi-arrow-right", action: JogPanelAction.xPositive, alt: "X Positive" }],
162
- * [{ icon: "pi pi-arrow-up-right", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
171
+ * [undefined, { icon: "jog_z_up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "jog_y_down_left", action: JogPanelAction.yNegative, alt: "Y Negative" }],
172
+ * [{ icon: "jog_x_left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "jog_x_right", action: JogPanelAction.xPositive, alt: "X Positive" }],
173
+ * [{ icon: "jog_y_up_right", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "jog_z_down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
163
174
  * ];
175
+ *
176
+ * // The icon is chosen by direction, not polarity: on a machine where
177
+ * // "X positive" jogs left, give that button `icon: "jog_x_left"` while
178
+ * // keeping `action: JogPanelAction.xPositive`.
164
179
  * ```
165
180
  *
166
181
  */
@@ -242,53 +257,42 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
242
257
  </div>
243
258
  }
244
259
 
245
- private actionToIcon(action : JogPanelAction | undefined) {
246
- if (action == JogPanelAction.Invalid) {
247
- return <i/>
248
- }
249
- else if (action == JogPanelAction.Stop) {
250
- return <i/>
251
- }
252
- else if (action == JogPanelAction.xNegative) {
253
- return <JogXNeg />
254
- }
255
- else if (action == JogPanelAction.xPositive) {
256
- return <JogXPos />
257
- }
258
- else if (action == JogPanelAction.yNegative) {
259
- return <JogYNeg />
260
- }
261
- else if (action == JogPanelAction.yPositive) {
262
- return <JogYPos />
263
- }
264
- else if (action == JogPanelAction.zNegative) {
265
- return <JogZNeg />
266
- }
267
- else if (action == JogPanelAction.zPositive) {
268
- return <JogZPos />
269
- }
270
- else if (action == JogPanelAction.aNegative) {
271
- return <RotationCcwA />
272
- }
273
- else if (action == JogPanelAction.aPositive) {
274
- return <RotationCwA />
275
- }
276
- else if (action == JogPanelAction.bNegative) {
277
- return <RotationCcwB />
278
- }
279
- else if (action == JogPanelAction.bPositive) {
280
- return <RotationCwB />
281
- }
282
- else if (action == JogPanelAction.cNegative) {
283
- return <RotationCcwC />
284
- }
285
- else if (action == JogPanelAction.cPositive) {
286
- return <RotationCwC />
287
- }
288
- else {
289
- return <i />
260
+ /**
261
+ * Map a button's `icon` direction key to its arrow glyph. Decoupled from
262
+ * `action` on purpose: which physical direction "jog X positive" moves is
263
+ * not consistent across machines, so the caller picks the icon by the
264
+ * direction the operator should see. Unknown/empty keys render nothing.
265
+ *
266
+ * The `pi pi-arrow-*` cases are legacy aliases kept so button definitions
267
+ * written before the direction-key scheme (which already encoded the
268
+ * arrow here — it was simply ignored) keep rendering the right glyph.
269
+ */
270
+ private selectIcon(icon : string | undefined) {
271
+ switch (icon) {
272
+ // Direction keys
273
+ case 'jog_x_left': return <JogXLeft />
274
+ case 'jog_x_right': return <JogXRight />
275
+ case 'jog_y_up_right': return <JogYUpRight />
276
+ case 'jog_y_down_left': return <JogYDownLeft />
277
+ case 'jog_z_up': return <JogZPos />
278
+ case 'jog_z_down': return <JogZNeg />
279
+ case 'rot_a_cw': return <RotationCwA />
280
+ case 'rot_a_ccw': return <RotationCcwA />
281
+ case 'rot_b_cw': return <RotationCwB />
282
+ case 'rot_b_ccw': return <RotationCcwB />
283
+ case 'rot_c_cw': return <RotationCwC />
284
+ case 'rot_c_ccw': return <RotationCcwC />
285
+
286
+ // Legacy PrimeReact arrow-class aliases (back-compat)
287
+ case 'pi pi-arrow-left': return <JogXLeft />
288
+ case 'pi pi-arrow-right': return <JogXRight />
289
+ case 'pi pi-arrow-up': return <JogZPos />
290
+ case 'pi pi-arrow-down': return <JogZNeg />
291
+ case 'pi pi-arrow-up-right': return <JogYUpRight />
292
+ case 'pi pi-arrow-down-left': return <JogYDownLeft />
293
+
294
+ default: return <i />
290
295
  }
291
-
292
296
  }
293
297
 
294
298
  standardButton(row : number, col : number) {
@@ -298,14 +302,15 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
298
302
  && this.props.buttonDefinitions[row][col] !== undefined
299
303
  && this.props.buttonDefinitions[row][col]?.action !== undefined ) {
300
304
 
301
- const action = this.props.buttonDefinitions[row][col]?.action;
305
+ const def = this.props.buttonDefinitions[row][col];
306
+ const action = def?.action;
302
307
 
303
308
  return <Button
304
309
  key={`${row}-${col}`}
305
- tooltip={this.props.buttonDefinitions[row][col]?.alt}
310
+ tooltip={def?.alt}
306
311
  onClick={() => this.handleClicked(action)}
307
312
  className="button-item"
308
- icon={() => this.actionToIcon(action)}/>
313
+ icon={() => this.selectIcon(def?.icon)}/>
309
314
 
310
315
  }
311
316
  else {
@@ -108,10 +108,17 @@ export const ValueInput: React.FC<ValueInputProps> = ({
108
108
  }
109
109
  }, [value, editing]);
110
110
 
111
+ // Treat the bounds as an unordered interval: an author may write the range
112
+ // in "magnitude" order (e.g. min:-50, max:-1000 for a compressive load),
113
+ // which is numerically inverted. Without this, min > max yields an empty
114
+ // valid set and the field rejects every value, including its own default.
115
+ const lo = min !== undefined && max !== undefined ? Math.min(min, max) : min;
116
+ const hi = min !== undefined && max !== undefined ? Math.max(min, max) : max;
117
+
111
118
  const validate = (val: number | null): boolean => {
112
119
  if (val === null || Number.isNaN(val)) return false;
113
- if (min !== undefined && val < min) return false;
114
- if (max !== undefined && val > max) return false;
120
+ if (lo !== undefined && val < lo) return false;
121
+ if (hi !== undefined && val > hi) return false;
115
122
  return true;
116
123
  };
117
124
 
@@ -148,7 +155,7 @@ export const ValueInput: React.FC<ValueInputProps> = ({
148
155
  };
149
156
 
150
157
  const isLabelDefined = label !== undefined && label !== null && label !== '';
151
- const allowNegative = min === undefined || min < 0;
158
+ const allowNegative = lo === undefined || lo < 0;
152
159
 
153
160
  return (
154
161
  <div>
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import React, { useEffect, useState } from 'react';
14
+ import { createPortal } from 'react-dom';
14
15
  import { Button } from 'primereact/button';
15
16
  import { useNetwork } from './NetworkProvider';
16
17
 
@@ -38,10 +39,23 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
38
39
  const total = Math.max(1, staged.revert_in_seconds);
39
40
  const pctRemaining = Math.max(0, Math.min(100, (remainingSec / total) * 100));
40
41
 
42
+ // NOTE: the banner is rendered through a portal into <body> (see the
43
+ // createPortal call below) and positioned `fixed`, not `sticky`. That is
44
+ // deliberate: host apps commonly wrap their content in a
45
+ // `position: fixed`/`transform`ed shell (which establishes a stacking
46
+ // context) and open configuration screens as PrimeReact *modal* dialogs.
47
+ // A modal Dialog portals its mask to <body> at z-index ~1100+, so a
48
+ // banner living inside the app shell — no matter how high its own
49
+ // z-index — renders *beneath* the modal and becomes unreachable
50
+ // ("press confirm on a popup that is hidden"). Portaling to <body> puts
51
+ // the banner in the root stacking context alongside the modal, where a
52
+ // z-index above the PrimeReact modal range actually wins.
41
53
  const containerStyle: React.CSSProperties = {
42
- position: 'sticky',
54
+ position: 'fixed',
43
55
  top: 0,
44
- zIndex: 1000,
56
+ left: 0,
57
+ right: 0,
58
+ zIndex: 2000,
45
59
  padding: '0.75rem 1rem',
46
60
  background: remainingSec <= 10 ? '#7f1d1d' : '#78350f',
47
61
  color: 'white',
@@ -50,7 +64,7 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
50
64
  ...style,
51
65
  };
52
66
 
53
- return (
67
+ const banner = (
54
68
  <div style={containerStyle} role="alertdialog" aria-live="polite">
55
69
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
56
70
  <div style={{ minWidth: 0 }}>
@@ -96,6 +110,11 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
96
110
  </div>
97
111
  </div>
98
112
  );
113
+
114
+ // Guard for non-DOM environments (SSR/tests); render inline if there is
115
+ // no document to portal into.
116
+ if (typeof document === 'undefined' || !document.body) return banner;
117
+ return createPortal(banner, document.body);
99
118
  };
100
119
 
101
120
  export default StagedChangeBanner;
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useContext } from 'react';
2
2
  import { DataTable } from 'primereact/datatable';
3
3
  import { Column } from 'primereact/column';
4
4
  import { Button } from 'primereact/button';
5
- import { Tag } from 'primereact/tag';
5
+ import { Badge } from 'primereact/badge';
6
6
  import { EventEmitterContext } from '../../core/EventEmitterContext';
7
7
  import { MessageType } from '../../hub/CommandMessage';
8
8
  import { useTis } from './TisProvider';
@@ -364,17 +364,26 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
364
364
  style={{ minWidth: '12rem' }}
365
365
  />
366
366
  <Column field="method_id" header="Test Method" sortable style={{ minWidth: '10rem' }} />
367
- <Column field="run_id" header="Run ID" sortable style={{ minWidth: '12rem' }} />
367
+ {/* Run ID dropped from the table — it's an internal handle
368
+ (the row's sample_id + date already identify the run for a
369
+ human), and its 12rem column was the main reason the table
370
+ overflowed in portrait. Still carried on the row data and
371
+ used for selection + downloads. */}
368
372
  <Column header="Status" field="status" sortable
369
373
  body={(rowData) => {
370
374
  const s = statusOf(rowData);
371
- return <Tag value={s.label} severity={s.severity} />;
375
+ return <Badge value={s.label} severity={s.severity} />;
372
376
  }}
373
- style={{ minWidth: '8rem' }}
377
+ style={{ minWidth: '6rem' }}
374
378
  />
379
+ {/* Inclusion shown as a single checkmark / eye-slash toggle so
380
+ the column stays narrow. Green check = counted in cross-test
381
+ results; amber eye-slash = excluded. Click toggles. */}
375
382
  <Column
376
- header="Include"
377
- style={{ width: '8rem' }}
383
+ header="Incl."
384
+ align="center"
385
+ alignHeader="center"
386
+ style={{ width: '4.5rem' }}
378
387
  body={(rowData) => {
379
388
  const isExcluded = rowData?.excluded === true;
380
389
  const isBusy = excludingRunId === rowData.run_id;
@@ -382,12 +391,14 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
382
391
  <Button
383
392
  icon={isBusy ? 'pi pi-spin pi-spinner'
384
393
  : (isExcluded ? 'pi pi-eye-slash' : 'pi pi-check')}
385
- label={isExcluded ? 'Excluded' : 'Included'}
394
+ rounded
395
+ text
386
396
  size="small"
387
- outlined
388
397
  severity={isExcluded ? 'warning' : 'success'}
389
398
  disabled={excludingRunId !== null}
390
399
  onClick={() => handleToggleExclude(rowData)}
400
+ aria-label={isExcluded ? 'Excluded — click to include'
401
+ : 'Included — click to exclude'}
391
402
  tooltip={isExcluded
392
403
  ? 'Excluded from cross-test results — click to include'
393
404
  : 'Included in cross-test results — click to exclude'}
@@ -0,0 +1,87 @@
1
+ /*
2
+ * ScienceTable — "instrument report" styling for cycle/results tables.
3
+ *
4
+ * Every rule is scoped under `.p-datatable.ac-science-table` (both classes on
5
+ * the DataTable root) so it out-specifies the PrimeReact theme's
6
+ * `.p-datatable ...` rules and cannot bleed onto any other table. The palette
7
+ * is a fixed light "lab report" look (dark headers, cream cells) independent
8
+ * of the app theme — that IS the requested Bluehill appearance.
9
+ */
10
+
11
+ .ac-science-table-wrap {
12
+ border: 1px solid #2b2b2b;
13
+ border-radius: 4px;
14
+ overflow: hidden;
15
+ background: #f7f4e9;
16
+ }
17
+
18
+ /* Dark title bar with a centered caption, like "Results 1". */
19
+ .ac-science-table-title {
20
+ background: #2b2b2b;
21
+ color: #ffffff;
22
+ text-align: center;
23
+ font-weight: 600;
24
+ letter-spacing: 0.02em;
25
+ padding: 0.4rem 0.75rem;
26
+ font-size: 0.95rem;
27
+ }
28
+
29
+ /* Horizontal scroll for wide results rows without pushing the page sideways. */
30
+ .ac-science-table-scroll {
31
+ overflow-x: auto;
32
+ }
33
+
34
+ /* ---- Column header band -------------------------------------------------- */
35
+ .p-datatable.ac-science-table .p-datatable-thead > tr > th {
36
+ background: #3d3d3d;
37
+ color: #ffffff;
38
+ text-align: center;
39
+ border: 1px solid #2b2b2b;
40
+ border-top: none;
41
+ padding: 0.35rem 0.6rem;
42
+ font-weight: 600;
43
+ vertical-align: middle;
44
+ }
45
+ .p-datatable.ac-science-table .p-datatable-thead > tr > th .p-column-header-content {
46
+ justify-content: center;
47
+ }
48
+
49
+ .ac-sci-th {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: center;
53
+ line-height: 1.15;
54
+ }
55
+ .ac-sci-th-unit {
56
+ font-weight: 400;
57
+ font-size: 0.8em;
58
+ opacity: 0.85;
59
+ }
60
+
61
+ /* ---- Data cells: cream, dark text, thin gridlines, tabular figures ------- */
62
+ .p-datatable.ac-science-table .p-datatable-tbody > tr {
63
+ background: #f7f4e9;
64
+ color: #1a1a1a;
65
+ }
66
+ .p-datatable.ac-science-table .p-datatable-tbody > tr > td {
67
+ background: #f7f4e9;
68
+ color: #1a1a1a;
69
+ text-align: center;
70
+ border: 1px solid #d9d2b8;
71
+ padding: 0.3rem 0.6rem;
72
+ font-variant-numeric: tabular-nums;
73
+ }
74
+ /* Faint zebra banding keeps long cycle lists readable on the cream base. */
75
+ .p-datatable.ac-science-table .p-datatable-tbody > tr:nth-child(even) > td {
76
+ background: #efe9d4;
77
+ }
78
+
79
+ /* ---- Row-index column: dark, matching the header band -------------------- */
80
+ .p-datatable.ac-science-table .p-datatable-thead > tr > th.ac-sci-index,
81
+ .p-datatable.ac-science-table .p-datatable-tbody > tr > td.ac-sci-index {
82
+ background: #3d3d3d;
83
+ color: #ffffff;
84
+ font-weight: 600;
85
+ text-align: center;
86
+ border: 1px solid #2b2b2b;
87
+ }