@adcops/autocore-react 3.6.8 → 3.6.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AssetEditDialog.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetEditDialog.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAkCxE,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB;;yDAEqD;IACrD,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AA8DD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAuqB1D,CAAC"}
1
+ {"version":3,"file":"AssetEditDialog.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetEditDialog.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA6BxE,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB;;yDAEqD;IACrD,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AA4CD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAuoB1D,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useState}from"react";import{Button}from"primereact/button";import{Calendar}from"primereact/calendar";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{TabPanel,TabView}from"primereact/tabview";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";const ROLE_OTHER="__other__";function fieldsFor(e,t){const r=e?.[t]?.fields;return Array.isArray(r)?r:[]}function subLocationsFor(e,t){const r=e?.[t]?.sub_locations;return r&&Array.isArray(r.keys)&&Array.isArray(r.fields)?r:null}function calibrationFieldsFor(e,t){const r=e?.[t]?.calibration_fields;return Array.isArray(r)?r:[]}function perAxisCalibrationFieldsFor(e){return e?.calibration_fields??[]}function coerceField(e,t){if(void 0!==t&&""!==t)switch(e.type){case"f32":case"f64":case"i8":case"i16":case"i32":case"i64":case"u8":case"u16":case"u32":case"u64":{const e=Number(t);return Number.isFinite(e)?e:void 0}case"bool":return"true"===t||"1"===t;default:return t}}function seedFromCustom(e,t){if(!e)return"";const r=e[t];return null==r?"":String(r)}export const AssetEditDialog=({visible:e,asset:t,onHide:r,onSaved:s})=>{const{schemas:n,roles:a,readCalibration:o}=useAms(),{invoke:i}=useContext(EventEmitterContext),l=t?.asset_type??"",d=useMemo(()=>fieldsFor(n,l),[n,l]),c=useMemo(()=>subLocationsFor(n,l),[n,l]),u=useMemo(()=>calibrationFieldsFor(n,l),[n,l]),m=useMemo(()=>perAxisCalibrationFieldsFor(c),[c]),p=m.length>0,_=p||u.length>0,f=useMemo(()=>l?a[l]??[]:[],[l,a]),x=f.length>0,h=useMemo(()=>{const e=f.map(e=>({label:e.label??e.location,value:e.location}));return e.push({label:"Other (advanced — type a custom role)",value:ROLE_OTHER}),e},[f]),[g,y]=useState(""),[b,v]=useState(""),[j,C]=useState(""),[T,S]=useState({}),[F,E]=useState({}),[w,A]=useState(!1),[R,k]=useState(null),[I,O]=useState(0),[$,L]=useState(null),[M,q]=useState(""),[D,H]=useState(null),[B,P]=useState(""),[z,N]=useState(""),[K,V]=useState({}),[W,X]=useState({});useEffect(()=>{if(!e||!t)return;k(null),A(!1),O(0),C("string"==typeof t.serial?t.serial:"");const r="string"==typeof t.location?t.location:"",s=f.find(e=>e.location===r);""===r?(y(""),v("")):s?(y(r),v(r)):(y(ROLE_OTHER),v(r));const n={};for(const e of d)n[e.name]=seedFromCustom(t.custom,e.name);S(n);const a={};if(c){const e=t.sub_locations&&"object"==typeof t.sub_locations?t.sub_locations:{};for(const t of c.keys){const r=e[t]&&"object"==typeof e[t]?e[t]:{},s={};for(const e of c.fields){const t=r[e.name];s[e.name]=null==t?"":String(t)}a[t]=s}}E(a),L(null),q(""),H(null),P(""),N(""),V({}),X({});const i="string"==typeof t.current_calibration_id?t.current_calibration_id:"";if(!i)return;let l=!1;return(async()=>{const e=await o(t.asset_id,i);if(l||!e)return;L(e),q("string"==typeof e.performed_by?e.performed_by:""),P("string"==typeof e.cert_ref?e.cert_ref:""),N("string"==typeof e.notes?e.notes:""),H(e.expires_at?new Date(e.expires_at):null);const r=e.values&&"object"==typeof e.values?e.values:{};if(p&&c){const e={};for(const t of c.keys){const s=r[t]&&"object"==typeof r[t]?r[t]:{},n={};for(const e of m){const t=s[e.name];n[e.name]=null==t?"":String(t)}e[t]=n}X(e)}else{const e={};for(const t of u){const s=r[t.name];e[t.name]=null==s?"":String(s)}V(e)}})(),()=>{l=!0}},[e,t,d,c,f,u,m,p,o]);const G=()=>{if(p&&c){const e={};for(const t of c.keys){const r={};for(const e of m){const s=coerceField(e,W[t]?.[e.name]??"");void 0!==s&&(r[e.name]=s)}e[t]=r}return e}const e={};for(const t of u){const r=coerceField(t,K[t.name]??"");void 0!==r&&(e[t.name]=r)}return e},J=!t?.asset_id||w||x&&(""===g||g===ROLE_OTHER&&!b.trim())||d.some(e=>e.required&&!T[e.name]?.toString().trim())||(c?.keys.some(e=>c.fields.some(t=>t.required&&!F[e]?.[t.name]?.toString().trim()))??!1);if(!t)return null;const Q=n[l]?.label??l;return _jsxs(Dialog,{header:`Edit Asset — ${t.asset_id}`,visible:e,style:{width:"32rem"},onHide:()=>{w||r()},footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:r,disabled:w}),_jsx(Button,{label:"Save",icon:"pi pi-check",onClick:async()=>{if(!t?.asset_id)return;A(!0),k(null);const e={};for(const t of d){const r=coerceField(t,T[t.name]??"");void 0!==r&&(e[t.name]=r)}let n;if(c){n={};for(const e of c.keys){const t={};for(const r of c.fields){const s=coerceField(r,F[e]?.[r.name]??"");void 0!==s&&(t[r.name]=s)}n[e]=t}}const a={asset_id:t.asset_id,location:b,serial:j,custom:e};n&&(a.sub_locations=n);try{const e=await i("ams.update_asset",MessageType.Request,a);if(!e?.success)return void k(e?.error_message??"update_asset failed");if($){const e={asset_id:t.asset_id,cal_id:$.cal_id,performed_by:M,expires_at:D?D.toISOString():null,cert_ref:B,notes:z,values:G()},r=await i("ams.update_calibration",MessageType.Request,e);if(!r?.success)return k(r?.error_message??"update_calibration failed"),void O(1)}s?.(),r()}catch(e){k(String(e?.message??e))}finally{A(!1)}},loading:w,disabled:J})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.25rem 1rem",fontSize:"0.875rem",color:"var(--text-secondary-color)",marginBottom:"0.75rem"},children:[_jsx("span",{children:"Type"})," ",_jsx("span",{children:Q}),t.install_date&&_jsxs(_Fragment,{children:[_jsx("span",{children:"Installed"}),_jsx("span",{children:new Date(t.install_date).toLocaleString()})]})]}),_jsxs(TabView,{activeIndex:I,onTabChange:e=>O(e.index),children:[_jsxs(TabPanel,{header:"Asset",children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Serial"}),_jsx(InputText,{value:j,placeholder:"(none)",onChange:e=>C(e.target.value)}),x&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Role *"}),_jsx(Dropdown,{value:g,options:h,onChange:e=>{var t;(t=e.value)===ROLE_OTHER?y(ROLE_OTHER):(y(t),v(t))},placeholder:"Choose where this asset is mounted"}),g===ROLE_OTHER&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Custom role"}),_jsx(InputText,{value:b,placeholder:"e.g. tsdr_secondary",onChange:e=>v(e.target.value)})]})]}),d.map(e=>{const t=e.label??e.name,r=e.units?`${t} [${e.units}]${e.required?" *":""}`:`${t}${e.required?" *":""}`,s="string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:r}),"bool"===e.type?_jsx(Dropdown,{value:T[e.name]??"",options:[{label:"true",value:"true"},{label:"false",value:"false"}],onChange:t=>S(r=>({...r,[e.name]:t.value})),placeholder:"—"}):_jsx(InputText,{value:T[e.name]??"",keyfilter:s?"num":void 0,placeholder:e.description??void 0,onChange:t=>S(r=>({...r,[e.name]:t.target.value}))})]},e.name)})]}),c&&_jsxs("div",{style:{marginTop:"1.5rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:c.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:c.key_label??"Key"}),c.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:c.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),c.fields.map(t=>{const r=F[e]?.[t.name]??"",s="string"!==t.type&&"bool"!==t.type;return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(InputText,{value:r,keyfilter:s?"num":void 0,onChange:r=>{const s=r.target.value;E(r=>({...r,[e]:{...r[e]??{},[t.name]:s}}))},style:{width:"100%"}})},t.name)})]},e))})]})})]})]}),$&&_jsxs(TabPanel,{header:`Calibration (${$.cal_id})`,children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Performed by"}),_jsx(InputText,{value:M,onChange:e=>q(e.target.value)}),_jsx("label",{children:"Expires at"}),_jsx(Calendar,{value:D,onChange:e=>H(e.value??null),showIcon:!0,dateFormat:"yy-mm-dd"}),_jsx("label",{children:"Certificate ref"}),_jsx(InputText,{value:B,onChange:e=>P(e.target.value),placeholder:"e.g. ADC-CAL-2026-1043"}),_jsx("label",{children:"Notes"}),_jsx(InputTextarea,{value:z,rows:3,onChange:e=>N(e.target.value),autoResize:!0})]}),_&&_jsxs("div",{style:{marginTop:"1rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:p?c?.label??"Per-axis calibration":"Calibration values"}),_jsx("div",p&&c?{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:c.key_label??"Key"}),m.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:c.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),m.map(t=>{const r=W[e]?.[t.name]??"",s="string"!==t.type&&"bool"!==t.type;return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(InputText,{value:r,keyfilter:s?"num":void 0,onChange:r=>{const s=r.target.value;X(r=>({...r,[e]:{...r[e]??{},[t.name]:s}}))},style:{width:"100%"}})},t.name)})]},e))})]})}:{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:u.map(e=>{const t=e.label??e.name,r=e.units?`${t} [${e.units}]${e.required?" *":""}`:`${t}${e.required?" *":""}`,s="string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:r}),_jsx(InputText,{value:K[e.name]??"",keyfilter:s?"num":void 0,onChange:t=>V(r=>({...r,[e.name]:t.target.value}))})]},e.name)})})]})]})]}),R&&_jsx("div",{style:{marginTop:"1rem",color:"#ef4444"},children:R})]})};
1
+ import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useState}from"react";import{Button}from"primereact/button";import{Calendar}from"primereact/calendar";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{TabPanel,TabView}from"primereact/tabview";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{SchemaFieldInput,coerceField,fieldLabel}from"./schemaField";const ROLE_OTHER="__other__";function fieldsFor(e,t){const r=e?.[t]?.fields;return Array.isArray(r)?r:[]}function subLocationsFor(e,t){const r=e?.[t]?.sub_locations;return r&&Array.isArray(r.keys)&&Array.isArray(r.fields)?r:null}function calibrationFieldsFor(e,t){const r=e?.[t]?.calibration_fields;return Array.isArray(r)?r:[]}function perAxisCalibrationFieldsFor(e){return e?.calibration_fields??[]}function seedFromCustom(e,t){if(!e)return"";const r=e[t];return null==r?"":String(r)}export const AssetEditDialog=({visible:e,asset:t,onHide:r,onSaved:s})=>{const{schemas:n,roles:a,readCalibration:o}=useAms(),{invoke:i}=useContext(EventEmitterContext),l=t?.asset_type??"",d=useMemo(()=>fieldsFor(n,l),[n,l]),c=useMemo(()=>subLocationsFor(n,l),[n,l]),m=useMemo(()=>calibrationFieldsFor(n,l),[n,l]),u=useMemo(()=>perAxisCalibrationFieldsFor(c),[c]),_=u.length>0,p=_||m.length>0,f=useMemo(()=>l?a[l]??[]:[],[l,a]),h=f.length>0,x=useMemo(()=>{const e=f.map(e=>({label:e.label??e.location,value:e.location}));return e.push({label:"Other (advanced — type a custom role)",value:ROLE_OTHER}),e},[f]),[g,b]=useState(""),[y,j]=useState(""),[v,C]=useState(""),[S,F]=useState({}),[T,E]=useState({}),[A,w]=useState(!1),[R,I]=useState(null),[O,k]=useState(0),[L,M]=useState(null),[H,B]=useState(""),[D,q]=useState(null),[P,z]=useState(""),[$,K]=useState(""),[V,W]=useState({}),[X,N]=useState({});useEffect(()=>{if(!e||!t)return;I(null),w(!1),k(0),C("string"==typeof t.serial?t.serial:"");const r="string"==typeof t.location?t.location:"",s=f.find(e=>e.location===r);""===r?(b(""),j("")):s?(b(r),j(r)):(b(ROLE_OTHER),j(r));const n={};for(const e of d)n[e.name]=seedFromCustom(t.custom,e.name);F(n);const a={};if(c){const e=t.sub_locations&&"object"==typeof t.sub_locations?t.sub_locations:{};for(const t of c.keys){const r=e[t]&&"object"==typeof e[t]?e[t]:{},s={};for(const e of c.fields){const t=r[e.name];s[e.name]=null==t?"":String(t)}a[t]=s}}E(a),M(null),B(""),q(null),z(""),K(""),W({}),N({});const i="string"==typeof t.current_calibration_id?t.current_calibration_id:"";if(!i)return;let l=!1;return(async()=>{const e=await o(t.asset_id,i);if(l||!e)return;M(e),B("string"==typeof e.performed_by?e.performed_by:""),z("string"==typeof e.cert_ref?e.cert_ref:""),K("string"==typeof e.notes?e.notes:""),q(e.expires_at?new Date(e.expires_at):null);const r=e.values&&"object"==typeof e.values?e.values:{};if(_&&c){const e={};for(const t of c.keys){const s=r[t]&&"object"==typeof r[t]?r[t]:{},n={};for(const e of u){const t=s[e.name];n[e.name]=null==t?"":String(t)}e[t]=n}N(e)}else{const e={};for(const t of m){const s=r[t.name];e[t.name]=null==s?"":String(s)}W(e)}})(),()=>{l=!0}},[e,t,d,c,f,m,u,_,o]);const G=()=>{if(_&&c){const e={};for(const t of c.keys){const r={};for(const e of u){const s=X[t]?.[e.name]??"",n=coerceField(e,s);void 0!==n&&(r[e.name]=n)}e[t]=r}return e}const e={};for(const t of m){const r=V[t.name],s=coerceField(t,r??"");void 0!==s&&(e[t.name]=s)}return e},J=!t?.asset_id||A||h&&(""===g||g===ROLE_OTHER&&!y.trim())||d.some(e=>e.required&&!S[e.name]?.toString().trim())||(c?.keys.some(e=>c.fields.some(t=>t.required&&!T[e]?.[t.name]?.toString().trim()))??!1);if(!t)return null;const Q=n[l]?.label??l;return _jsxs(Dialog,{header:`Edit Asset — ${t.asset_id}`,visible:e,style:{width:"32rem"},onHide:()=>{A||r()},footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:r,disabled:A}),_jsx(Button,{label:"Save",icon:"pi pi-check",onClick:async()=>{if(!t?.asset_id)return;w(!0),I(null);const e={};for(const t of d){const r=S[t.name],s=coerceField(t,r??"");void 0!==s&&(e[t.name]=s)}let n;if(c){n={};for(const e of c.keys){const t={};for(const r of c.fields){const s=T[e]?.[r.name]??"",n=coerceField(r,s);void 0!==n&&(t[r.name]=n)}n[e]=t}}const a={asset_id:t.asset_id,location:y,serial:v,custom:e};n&&(a.sub_locations=n);try{const e=await i("ams.update_asset",MessageType.Request,a);if(!e?.success)return void I(e?.error_message??"update_asset failed");if(L){const e={asset_id:t.asset_id,cal_id:L.cal_id,performed_by:H,expires_at:D?D.toISOString():null,cert_ref:P,notes:$,values:G()},r=await i("ams.update_calibration",MessageType.Request,e);if(!r?.success)return I(r?.error_message??"update_calibration failed"),void k(1)}s?.(),r()}catch(e){I(String(e?.message??e))}finally{w(!1)}},loading:A,disabled:J})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.25rem 1rem",fontSize:"0.875rem",color:"var(--text-secondary-color)",marginBottom:"0.75rem"},children:[_jsx("span",{children:"Type"})," ",_jsx("span",{children:Q}),t.install_date&&_jsxs(_Fragment,{children:[_jsx("span",{children:"Installed"}),_jsx("span",{children:new Date(t.install_date).toLocaleString()})]})]}),_jsxs(TabView,{activeIndex:O,onTabChange:e=>k(e.index),children:[_jsxs(TabPanel,{header:"Asset",children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Serial"}),_jsx(InputText,{value:v,placeholder:"(none)",onChange:e=>C(e.target.value)}),h&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Role *"}),_jsx(Dropdown,{value:g,options:x,onChange:e=>{var t;(t=e.value)===ROLE_OTHER?b(ROLE_OTHER):(b(t),j(t))},placeholder:"Choose where this asset is mounted"}),g===ROLE_OTHER&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Custom role"}),_jsx(InputText,{value:y,placeholder:"e.g. tsdr_secondary",onChange:e=>j(e.target.value)})]})]}),d.map(e=>_jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:fieldLabel(e)}),_jsx(SchemaFieldInput,{field:e,value:S[e.name]??"",onChange:t=>F(r=>({...r,[e.name]:t}))})]},e.name))]}),c&&_jsxs("div",{style:{marginTop:"1.5rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:c.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:c.key_label??"Key"}),c.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:c.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),c.fields.map(t=>{const r=T[e]?.[t.name]??"";return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(SchemaFieldInput,{field:t,value:r,onChange:r=>{E(s=>({...s,[e]:{...s[e]??{},[t.name]:r}}))},style:{width:"100%"}})},t.name)})]},e))})]})})]})]}),L&&_jsxs(TabPanel,{header:`Calibration (${L.cal_id})`,children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Performed by"}),_jsx(InputText,{value:H,onChange:e=>B(e.target.value)}),_jsx("label",{children:"Expires at"}),_jsx(Calendar,{value:D,onChange:e=>q(e.value??null),showIcon:!0,dateFormat:"yy-mm-dd"}),_jsx("label",{children:"Certificate ref"}),_jsx(InputText,{value:P,onChange:e=>z(e.target.value),placeholder:"e.g. ADC-CAL-2026-1043"}),_jsx("label",{children:"Notes"}),_jsx(InputTextarea,{value:$,rows:3,onChange:e=>K(e.target.value),autoResize:!0})]}),p&&_jsxs("div",{style:{marginTop:"1rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:_?c?.label??"Per-axis calibration":"Calibration values"}),_jsx("div",_&&c?{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:c.key_label??"Key"}),u.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:c.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),u.map(t=>{const r=X[e]?.[t.name]??"";return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(SchemaFieldInput,{field:t,value:r,onChange:r=>{N(s=>({...s,[e]:{...s[e]??{},[t.name]:r}}))},style:{width:"100%"}})},t.name)})]},e))})]})}:{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:m.map(e=>_jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:fieldLabel(e)}),_jsx(SchemaFieldInput,{field:e,value:V[e.name]??"",onChange:t=>W(r=>({...r,[e.name]:t}))})]},e.name))})]})]})]}),R&&_jsx("div",{style:{marginTop:"1rem",color:"#ef4444"},children:R})]})};
@@ -1 +1 @@
1
- {"version":3,"file":"AssetRegistryTable.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetRegistryTable.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAUxE,OAAO,0BAA0B,CAAC;AA8IlC,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAijBtC,CAAC"}
1
+ {"version":3,"file":"AssetRegistryTable.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetRegistryTable.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,0BAA0B,CAAC;AAoHlC,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAuhBtC,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import"./AssetRegistryTable.css";const ROLE_OTHER="__other__",EMPTY_ADD={open:!1,assetType:"",serial:"",roleSelection:"",location:"",customFields:{},subLocationFields:{}};function roleLabel(e){return e.label??e.location}function roleUsageSummary(e){const s=[];return e.used_by.length>0&&s.push(`Used by test method${1===e.used_by.length?"":"s"}: ${e.used_by.join(", ")}`),e.used_by_modules.length>0&&s.push(`Used by module${1===e.used_by_modules.length?"":"s"}: ${e.used_by_modules.join(", ")}`),0===s.length?"Not referenced by any test method or module.":s.join(" • ")}function fieldsFor(e,s){const t=e?.[s]?.fields;return Array.isArray(t)?t:[]}function subLocationsFor(e,s){const t=e?.[s]?.sub_locations;return t&&Array.isArray(t.keys)&&Array.isArray(t.fields)?t:null}function seedCustomFromRoleDefaults(e,s){const t={};if(!e||!e.defaults)return t;const o=new Set(s.map(e=>e.name));for(const[s,l]of Object.entries(e.defaults))o.has(s)&&null!=l&&(t[s]=String(l));return t}function coerceField(e,s){if(void 0!==s&&""!==s)switch(e.type){case"f32":case"f64":case"i8":case"i16":case"i32":case"i64":case"u8":case"u16":case"u32":case"u64":{const e=Number(s);return Number.isFinite(e)?e:void 0}case"bool":return"true"===s||"1"===s;default:return s}}export const AssetRegistryTable=()=>{const{schemas:e,roles:s,assets:t,refreshAssets:o,setSelection:l,selection:a}=useAms(),{invoke:n}=useContext(EventEmitterContext),[i,r]=useState(null),[c,d]=useState(null),[u,m]=useState(EMPTY_ADD),[p,y]=useState(!1);useEffect(()=>{const t=t=>{const o=t.detail,l=o?.assetType??"",a=o?.location??"";if(!l)return;const n=(s[l]??[]).find(e=>e.location===a),i=fieldsFor(e,l);m({...EMPTY_ADD,open:!0,assetType:l,roleSelection:a,location:a,customFields:seedCustomFromRoleDefaults(n,i)})};return window.addEventListener("ams:prefill-add",t),()=>window.removeEventListener("ams:prefill-add",t)},[s,e]);const _=useMemo(()=>Object.keys(e).map(s=>({label:e[s]?.label??s,value:s})),[e]),h=useMemo(()=>u.assetType?s[u.assetType]??[]:[],[u.assetType,s]),f=h.length>0,b=useMemo(()=>{const e=h.map(e=>({label:roleLabel(e),value:e.location}));return e.push({label:"Other (advanced — type a custom role)",value:ROLE_OTHER}),e},[h]),x=useMemo(()=>u.roleSelection===ROLE_OTHER?null:h.find(e=>e.location===u.roleSelection)??null,[u.roleSelection,h]),g=useMemo(()=>t.filter(e=>(!i||e.asset_type===i)&&(!c||e.status===c)),[t,i,c]),j=useMemo(()=>fieldsFor(e,u.assetType),[e,u.assetType]),v=useMemo(()=>subLocationsFor(e,u.assetType),[e,u.assetType]),T=!u.assetType||f&&(""===u.roleSelection||u.roleSelection===ROLE_OTHER&&!u.location.trim())||j.some(e=>e.required&&!u.customFields[e.name]?.toString().trim())||(v?.keys.some(e=>v.fields.some(s=>s.required&&!u.subLocationFields[e]?.[s.name]?.toString().trim()))??!1);return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{style:{display:"flex",gap:"0.75rem",alignItems:"center"},children:[_jsx(Dropdown,{value:i,options:[{label:"All Types",value:null},..._],onChange:e=>r(e.value),placeholder:"Filter by type"}),_jsx(Dropdown,{value:c,options:[{label:"All Statuses",value:null},{label:"Active",value:"active"},{label:"Out for Service",value:"out_for_service"},{label:"Retired",value:"retired"}],onChange:e=>d(e.value),placeholder:"Filter by status"}),_jsx("span",{style:{marginLeft:"auto"},children:_jsx(Button,{icon:"pi pi-plus",label:"Add Asset",onClick:()=>m(e=>({...e,open:!0})),disabled:0===_.length})})]}),_jsxs(DataTable,{value:g,selectionMode:"single",selection:g.find(e=>e.asset_id===a.assetId)??null,onSelectionChange:e=>{const s=e.value;s&&l({assetType:s.asset_type,assetId:s.asset_id})},dataKey:"asset_id",emptyMessage:0===_.length?"AMS not enabled in this project (no asset_types declared).":"No assets registered yet.",stripedRows:!0,className:"ams-asset-table",children:[_jsx(Column,{field:"asset_id",header:"Asset ID"}),_jsx(Column,{field:"asset_type",header:"Type",body:s=>e[s.asset_type]?.label??s.asset_type}),_jsx(Column,{field:"serial",header:"Serial"}),_jsx(Column,{field:"location",header:"Role",body:e=>e.location?((e,t)=>{const o=(s[e]??[]).find(e=>e.location===t);return o?roleLabel(o):t})(e.asset_type,e.location):_jsx("span",{style:{color:"#9ca3af"},children:"—"})}),_jsx(Column,{field:"status",header:"Status"}),_jsx(Column,{header:"Calibration",body:e=>e.current_calibration_id?_jsx("span",{title:e.current_calibration_id,children:"✓"}):_jsx("span",{style:{color:"#f59e0b"},children:"none"})})]}),_jsxs(Dialog,{header:"Add New Asset",visible:u.open,style:{width:"32rem"},closable:!p,closeOnEscape:!p,onHide:()=>{p||m(EMPTY_ADD)},footer:p?_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",alignItems:"center",gap:"0.5rem"},children:[_jsx("i",{className:"pi pi-spin pi-spinner"}),_jsx("span",{children:"Creating asset…"})]}):_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:()=>m(EMPTY_ADD)}),_jsx(Button,{label:"Create",icon:"pi pi-check",onClick:async()=>{if(!u.assetType||p)return;const e={};for(const s of j){const t=u.customFields[s.name];if(void 0!==t&&""!==t)switch(s.type){case"f32":case"f64":case"i8":case"i16":case"i32":case"i64":case"u8":case"u16":case"u32":case"u64":{const o=Number(t);Number.isFinite(o)&&(e[s.name]=o);break}case"bool":e[s.name]="true"===t||"1"===t;break;default:e[s.name]=t}}const s={};if(v)for(const e of v.keys){const t={};for(const s of v.fields){const o=coerceField(s,u.subLocationFields[e]?.[s.name]??"");void 0!==o&&(t[s.name]=o)}s[e]=t}const t={asset_type:u.assetType,serial:u.serial,location:u.location,custom:e};v&&(t.sub_locations=s),y(!0);try{const e=await n("ams.create_asset",MessageType.Request,t);e?.success&&(m(EMPTY_ADD),await o(),e.data?.asset_id&&l({assetType:u.assetType,assetId:e.data.asset_id}))}catch(e){}finally{y(!1)}},disabled:T})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Type *"}),_jsx(Dropdown,{value:u.assetType,options:_,onChange:t=>(t=>{const o=s[t]??[],l=1===o.length?o[0]:void 0,a=l?{roleSelection:l.location,location:l.location}:{roleSelection:"",location:""},n=fieldsFor(e,t);m(e=>({...e,assetType:t,...a,customFields:seedCustomFromRoleDefaults(l,n),subLocationFields:{}}))})(t.value),placeholder:"Choose asset type"}),u.assetType&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Serial"}),_jsx(InputText,{value:u.serial,onChange:e=>m(s=>({...s,serial:e.target.value}))})]}),f&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Role *"}),_jsx(Dropdown,{value:u.roleSelection,options:b,onChange:e=>(e=>{if(e===ROLE_OTHER)return void m(e=>({...e,roleSelection:ROLE_OTHER,location:""}));const s=h.find(s=>s.location===e);m(t=>({...t,roleSelection:e,location:e,customFields:seedCustomFromRoleDefaults(s,j)}))})(e.value),placeholder:"Choose where this asset is mounted"}),u.roleSelection===ROLE_OTHER&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Custom role"}),_jsx(InputText,{value:u.location,placeholder:"e.g. tsdr_secondary",onChange:e=>m(s=>({...s,location:e.target.value}))})]})]}),j.map(e=>{const s=e.label??e.name,t=e.units?`${s} [${e.units}]${e.required?" *":""}`:`${s}${e.required?" *":""}`,o="string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:t}),"bool"===e.type?_jsx(Dropdown,{value:u.customFields[e.name]??"",options:[{label:"true",value:"true"},{label:"false",value:"false"}],onChange:s=>m(t=>({...t,customFields:{...t.customFields,[e.name]:s.value}})),placeholder:"—"}):_jsx(InputText,{value:u.customFields[e.name]??"",keyfilter:o?"num":void 0,placeholder:e.description??void 0,onChange:s=>m(t=>({...t,customFields:{...t.customFields,[e.name]:s.target.value}}))})]},e.name)})]}),v&&_jsxs("div",{style:{marginTop:"1.5rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:v.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:v.key_label??"Key"}),v.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:v.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),v.fields.map(s=>{const t=u.subLocationFields[e]?.[s.name]??"",o="string"!==s.type&&"bool"!==s.type;return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(InputText,{value:t,keyfilter:o?"num":void 0,onChange:t=>{const o=t.target.value;m(t=>({...t,subLocationFields:{...t.subLocationFields,[e]:{...t.subLocationFields[e]??{},[s.name]:o}}}))},style:{width:"100%"}})},s.name)})]},e))})]})})]}),_jsxs("div",{style:{fontSize:"0.875rem",color:"#9ca3af",marginTop:"1rem"},children:[f&&x&&_jsxs(_Fragment,{children:[x.description&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:x.description}),_jsxs("p",{style:{margin:"0 0 0.5rem 0",color:"#34d399"},children:["✓ ",roleUsageSummary(x)]})]}),f&&u.roleSelection===ROLE_OTHER&&_jsx("p",{style:{margin:"0 0 0.5rem 0",color:"#f59e0b"},children:"⚠ No test method on this machine references this custom role yet. The asset will be registered but won't be picked up at start_test unless you add an asset_ref to a test method's project.json."}),!f&&u.assetType&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"This asset type isn't selected by physical role on this machine — test methods reference it by ID instead. Just register the asset and pick it by ID on the Test Setup form when running a test."}),_jsxs("p",{style:{margin:0},children:["The asset_id is generated by the server (format:"," ",_jsxs("code",{children:[u.assetType?e[u.assetType]?.id_prefix??"A-":"A-","YYYYMMDDTHHMMSS"]}),"). Manufacturer serial is recorded for traceability but is not used as the unique key."]})]})]})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{SchemaFieldInput,coerceField,fieldLabel}from"./schemaField";import"./AssetRegistryTable.css";const ROLE_OTHER="__other__",EMPTY_ADD={open:!1,assetType:"",serial:"",roleSelection:"",location:"",customFields:{},subLocationFields:{}};function roleLabel(e){return e.label??e.location}function roleUsageSummary(e){const s=[];return e.used_by.length>0&&s.push(`Used by test method${1===e.used_by.length?"":"s"}: ${e.used_by.join(", ")}`),e.used_by_modules.length>0&&s.push(`Used by module${1===e.used_by_modules.length?"":"s"}: ${e.used_by_modules.join(", ")}`),0===s.length?"Not referenced by any test method or module.":s.join(" • ")}function fieldsFor(e,s){const t=e?.[s]?.fields;return Array.isArray(t)?t:[]}function subLocationsFor(e,s){const t=e?.[s]?.sub_locations;return t&&Array.isArray(t.keys)&&Array.isArray(t.fields)?t:null}function seedCustomFromRoleDefaults(e,s){const t={};if(!e||!e.defaults)return t;const o=new Set(s.map(e=>e.name));for(const[s,l]of Object.entries(e.defaults))o.has(s)&&null!=l&&(t[s]=String(l));return t}export const AssetRegistryTable=()=>{const{schemas:e,roles:s,assets:t,refreshAssets:o,setSelection:l,selection:a}=useAms(),{invoke:i}=useContext(EventEmitterContext),[n,r]=useState(null),[c,d]=useState(null),[u,m]=useState(EMPTY_ADD),[p,_]=useState(!1);useEffect(()=>{const t=t=>{const o=t.detail,l=o?.assetType??"",a=o?.location??"";if(!l)return;const i=(s[l]??[]).find(e=>e.location===a),n=fieldsFor(e,l);m({...EMPTY_ADD,open:!0,assetType:l,roleSelection:a,location:a,customFields:seedCustomFromRoleDefaults(i,n)})};return window.addEventListener("ams:prefill-add",t),()=>window.removeEventListener("ams:prefill-add",t)},[s,e]);const h=useMemo(()=>Object.keys(e).map(s=>({label:e[s]?.label??s,value:s})),[e]),y=useMemo(()=>u.assetType?s[u.assetType]??[]:[],[u.assetType,s]),f=y.length>0,b=useMemo(()=>{const e=y.map(e=>({label:roleLabel(e),value:e.location}));return e.push({label:"Other (advanced — type a custom role)",value:ROLE_OTHER}),e},[y]),x=useMemo(()=>u.roleSelection===ROLE_OTHER?null:y.find(e=>e.location===u.roleSelection)??null,[u.roleSelection,y]),g=useMemo(()=>t.filter(e=>(!n||e.asset_type===n)&&(!c||e.status===c)),[t,n,c]),j=useMemo(()=>fieldsFor(e,u.assetType),[e,u.assetType]),v=useMemo(()=>subLocationsFor(e,u.assetType),[e,u.assetType]),T=!u.assetType||f&&(""===u.roleSelection||u.roleSelection===ROLE_OTHER&&!u.location.trim())||j.some(e=>e.required&&!u.customFields[e.name]?.toString().trim())||(v?.keys.some(e=>v.fields.some(s=>s.required&&!u.subLocationFields[e]?.[s.name]?.toString().trim()))??!1);return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{style:{display:"flex",gap:"0.75rem",alignItems:"center"},children:[_jsx(Dropdown,{value:n,options:[{label:"All Types",value:null},...h],onChange:e=>r(e.value),placeholder:"Filter by type"}),_jsx(Dropdown,{value:c,options:[{label:"All Statuses",value:null},{label:"Active",value:"active"},{label:"Out for Service",value:"out_for_service"},{label:"Retired",value:"retired"}],onChange:e=>d(e.value),placeholder:"Filter by status"}),_jsx("span",{style:{marginLeft:"auto"},children:_jsx(Button,{icon:"pi pi-plus",label:"Add Asset",onClick:()=>m(e=>({...e,open:!0})),disabled:0===h.length})})]}),_jsxs(DataTable,{value:g,selectionMode:"single",selection:g.find(e=>e.asset_id===a.assetId)??null,onSelectionChange:e=>{const s=e.value;s&&l({assetType:s.asset_type,assetId:s.asset_id})},dataKey:"asset_id",emptyMessage:0===h.length?"AMS not enabled in this project (no asset_types declared).":"No assets registered yet.",stripedRows:!0,className:"ams-asset-table",children:[_jsx(Column,{field:"asset_id",header:"Asset ID"}),_jsx(Column,{field:"asset_type",header:"Type",body:s=>e[s.asset_type]?.label??s.asset_type}),_jsx(Column,{field:"serial",header:"Serial"}),_jsx(Column,{field:"location",header:"Role",body:e=>e.location?((e,t)=>{const o=(s[e]??[]).find(e=>e.location===t);return o?roleLabel(o):t})(e.asset_type,e.location):_jsx("span",{style:{color:"#9ca3af"},children:"—"})}),_jsx(Column,{field:"status",header:"Status"}),_jsx(Column,{header:"Calibration",body:e=>e.current_calibration_id?_jsx("span",{title:e.current_calibration_id,children:"✓"}):_jsx("span",{style:{color:"#f59e0b"},children:"none"})})]}),_jsxs(Dialog,{header:"Add New Asset",visible:u.open,style:{width:"32rem"},closable:!p,closeOnEscape:!p,onHide:()=>{p||m(EMPTY_ADD)},footer:p?_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",alignItems:"center",gap:"0.5rem"},children:[_jsx("i",{className:"pi pi-spin pi-spinner"}),_jsx("span",{children:"Creating asset…"})]}):_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:()=>m(EMPTY_ADD)}),_jsx(Button,{label:"Create",icon:"pi pi-check",onClick:async()=>{if(!u.assetType||p)return;const e={};for(const s of j){const t=u.customFields[s.name];if(void 0!==t&&""!==t)switch(s.type){case"f32":case"f64":case"i8":case"i16":case"i32":case"i64":case"u8":case"u16":case"u32":case"u64":{const o=Number(t);Number.isFinite(o)&&(e[s.name]=o);break}case"bool":e[s.name]="true"===t||"1"===t;break;default:e[s.name]=t}}const s={};if(v)for(const e of v.keys){const t={};for(const s of v.fields){const o=u.subLocationFields[e]?.[s.name]??"",l=coerceField(s,o);void 0!==l&&(t[s.name]=l)}s[e]=t}const t={asset_type:u.assetType,serial:u.serial,location:u.location,custom:e};v&&(t.sub_locations=s),_(!0);try{const e=await i("ams.create_asset",MessageType.Request,t);e?.success&&(m(EMPTY_ADD),await o(),e.data?.asset_id&&l({assetType:u.assetType,assetId:e.data.asset_id}))}catch(e){}finally{_(!1)}},disabled:T})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Type *"}),_jsx(Dropdown,{value:u.assetType,options:h,onChange:t=>(t=>{const o=s[t]??[],l=1===o.length?o[0]:void 0,a=l?{roleSelection:l.location,location:l.location}:{roleSelection:"",location:""},i=fieldsFor(e,t);m(e=>({...e,assetType:t,...a,customFields:seedCustomFromRoleDefaults(l,i),subLocationFields:{}}))})(t.value),placeholder:"Choose asset type"}),u.assetType&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Serial"}),_jsx(InputText,{value:u.serial,onChange:e=>m(s=>({...s,serial:e.target.value}))})]}),f&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Role *"}),_jsx(Dropdown,{value:u.roleSelection,options:b,onChange:e=>(e=>{if(e===ROLE_OTHER)return void m(e=>({...e,roleSelection:ROLE_OTHER,location:""}));const s=y.find(s=>s.location===e);m(t=>({...t,roleSelection:e,location:e,customFields:seedCustomFromRoleDefaults(s,j)}))})(e.value),placeholder:"Choose where this asset is mounted"}),u.roleSelection===ROLE_OTHER&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Custom role"}),_jsx(InputText,{value:u.location,placeholder:"e.g. tsdr_secondary",onChange:e=>m(s=>({...s,location:e.target.value}))})]})]}),j.map(e=>_jsxs(React.Fragment,{children:[_jsx("label",{title:e.description??void 0,children:fieldLabel(e)}),_jsx(SchemaFieldInput,{field:e,value:u.customFields[e.name]??"",onChange:s=>m(t=>({...t,customFields:{...t.customFields,[e.name]:s}}))})]},e.name))]}),v&&_jsxs("div",{style:{marginTop:"1.5rem"},children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:v.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:v.key_label??"Key"}),v.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:"",e.required?" *":""]},e.name))]})}),_jsx("tbody",{children:v.keys.map(e=>_jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:e}),v.fields.map(s=>{const t=u.subLocationFields[e]?.[s.name]??"";return _jsx("td",{style:{padding:"0.25rem 0.5rem"},children:_jsx(SchemaFieldInput,{field:s,value:t,onChange:t=>{m(o=>({...o,subLocationFields:{...o.subLocationFields,[e]:{...o.subLocationFields[e]??{},[s.name]:t}}}))},style:{width:"100%"}})},s.name)})]},e))})]})})]}),_jsxs("div",{style:{fontSize:"0.875rem",color:"#9ca3af",marginTop:"1rem"},children:[f&&x&&_jsxs(_Fragment,{children:[x.description&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:x.description}),_jsxs("p",{style:{margin:"0 0 0.5rem 0",color:"#34d399"},children:["✓ ",roleUsageSummary(x)]})]}),f&&u.roleSelection===ROLE_OTHER&&_jsx("p",{style:{margin:"0 0 0.5rem 0",color:"#f59e0b"},children:"⚠ No test method on this machine references this custom role yet. The asset will be registered but won't be picked up at start_test unless you add an asset_ref to a test method's project.json."}),!f&&u.assetType&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"This asset type isn't selected by physical role on this machine — test methods reference it by ID instead. Just register the asset and pick it by ID on the Test Setup form when running a test."}),_jsxs("p",{style:{margin:0},children:["The asset_id is generated by the server (format:"," ",_jsxs("code",{children:[u.assetType?e[u.assetType]?.id_prefix??"A-":"A-","YYYYMMDDTHHMMSS"]}),"). Manufacturer serial is recorded for traceability but is not used as the unique key."]})]})]})]})};
@@ -10,6 +10,8 @@ export { MissingAssetsBanner } from './MissingAssetsBanner';
10
10
  export { InstalledAssetPicker } from './InstalledAssetPicker';
11
11
  export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
12
12
  export { AssetNameplateGrid } from './AssetNameplateGrid';
13
+ export { SchemaFieldInput, coerceField, fieldChoices, fieldLabel, isNumericField } from './schemaField';
14
+ export type { SchemaField, FieldOption, Choice, SchemaFieldInputProps } from './schemaField';
13
15
  export { useInstalledAsset } from './useInstalledAsset';
14
16
  export type { InstalledAssetState } from './useInstalledAsset';
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7H,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7H,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI1D,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxG,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";export{InstalledAssetPicker}from"./InstalledAssetPicker";export{AssetNameplateGrid}from"./AssetNameplateGrid";export{useInstalledAsset}from"./useInstalledAsset";
1
+ export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";export{InstalledAssetPicker}from"./InstalledAssetPicker";export{AssetNameplateGrid}from"./AssetNameplateGrid";export{SchemaFieldInput,coerceField,fieldChoices,fieldLabel,isNumericField}from"./schemaField";export{useInstalledAsset}from"./useInstalledAsset";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file
3
+ * One asset-schema field: its shape, how a typed value is coerced out of the
4
+ * string the form holds, and the input control that edits it.
5
+ *
6
+ * Every AMS form renders the same thing in seven places — the asset create
7
+ * form, the asset edit form, the per-axis sub-location matrix in each of those,
8
+ * the per-axis calibration matrix, the flat calibration values, and the
9
+ * standalone calibration dialog. They had drifted into seven copies of the same
10
+ * `label ?? name` / `[units]` / `*` / bool-Dropdown-else-InputText block, plus
11
+ * three copies of `SchemaField` and two of `coerceField`.
12
+ *
13
+ * That duplication is why a field could not gain a behaviour: adding one meant
14
+ * finding all seven. `capacity_units` is the case that forced this — it is a
15
+ * free-text string today, so an operator typed "Newtons" where the consuming
16
+ * machine matched on "N", and the fix is a dropdown in every one of those
17
+ * places or it is not a fix.
18
+ */
19
+ import React from 'react';
20
+ /**
21
+ * One choice in a field's dropdown, as authored in the asset-type schema.
22
+ *
23
+ * Mirrors `FieldOption` in autocore-ams's `config.rs`: either a bare scalar,
24
+ * where the stored value and the operator-facing label are the same, or an
25
+ * explicit pair when they should differ.
26
+ *
27
+ * ```jsonc
28
+ * "options": ["N", "kN", "lbf"]
29
+ * "options": [{ "label": "Newtons (N)", "value": "N" }]
30
+ * ```
31
+ */
32
+ export type FieldOption = string | number | boolean | {
33
+ label: string;
34
+ value: unknown;
35
+ };
36
+ /** One field of an asset type's `fields` / `calibration_fields` schema. */
37
+ export interface SchemaField {
38
+ name: string;
39
+ type: string;
40
+ units?: string;
41
+ label?: string;
42
+ description?: string;
43
+ required?: boolean;
44
+ /** Fixed set of choices. When present the field is a dropdown, not a
45
+ * free-form input — see the file header. */
46
+ options?: FieldOption[];
47
+ }
48
+ /** A choice normalised for PrimeReact's `Dropdown`. */
49
+ export interface Choice {
50
+ label: string;
51
+ value: string;
52
+ }
53
+ /**
54
+ * The choices this field offers, or `null` if it is free-form.
55
+ *
56
+ * Booleans are a fixed two-choice dropdown, as they always were. An explicit
57
+ * `options` list wins over the type's own rendering, which is what lets a
58
+ * `string` field be constrained without inventing a new type.
59
+ *
60
+ * Values are carried as STRINGS because that is what the form state holds;
61
+ * `coerceField` converts back on submit.
62
+ */
63
+ export declare function fieldChoices(field: SchemaField): Choice[] | null;
64
+ /** True when the field takes digits only, so the input can keyfilter. A field
65
+ * with `options` is never free-typed, so it never keyfilters. */
66
+ export declare function isNumericField(field: SchemaField): boolean;
67
+ /**
68
+ * Coerce the form's string back to the field's declared type.
69
+ *
70
+ * Numeric strings become JSON numbers and booleans become bools, because the
71
+ * placeholder resolver and the SDO writers downstream expect native types — a
72
+ * string `"5000"` does not satisfy a `${ams.*}` reference asking for a numeric
73
+ * `max_val`. Empty stays `undefined` so an untouched field is omitted from the
74
+ * record rather than stored blank.
75
+ */
76
+ export declare function coerceField(field: SchemaField, raw: string | undefined): any;
77
+ /** The label a form shows for this field: pretty name, units, required mark. */
78
+ export declare function fieldLabel(field: SchemaField): string;
79
+ export interface SchemaFieldInputProps {
80
+ field: SchemaField;
81
+ /** Current value, as the string the form state holds. */
82
+ value: string;
83
+ /** Called with the new string. Coercion happens on submit, not here. */
84
+ onChange: (next: string) => void;
85
+ style?: React.CSSProperties;
86
+ }
87
+ /**
88
+ * The input control for one schema field: a dropdown when the field has a
89
+ * fixed set of choices, a text box otherwise.
90
+ *
91
+ * Deliberately uncontrolled about layout — callers place it in a grid or a
92
+ * table cell — so this owns only the decision that must be the same everywhere.
93
+ */
94
+ export declare const SchemaFieldInput: React.FC<SchemaFieldInputProps>;
95
+ //# sourceMappingURL=schemaField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaField.d.ts","sourceRoot":"","sources":["../../../src/components/ams/schemaField.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,MAAM,GACN,OAAO,GACP;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAExC,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;iDAC6C;IAC7C,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,uDAAuD;AACvD,MAAM,WAAW,MAAM;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,GAAG,IAAI,CAchE;AAED;kEACkE;AAClE,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAG1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,CAc5E;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAIrD;AAED,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,WAAW,CAAC;IACnB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAwB5D,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx}from"react/jsx-runtime";import React from"react";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";export function fieldChoices(e){return e.options&&e.options.length>0?e.options.map(e=>null!==e&&"object"==typeof e&&"value"in e?{label:String(e.label),value:String(e.value)}:{label:String(e),value:String(e)}):"bool"===e.type?[{label:"true",value:"true"},{label:"false",value:"false"}]:null}export function isNumericField(e){return!fieldChoices(e)&&("string"!==e.type&&"bool"!==e.type)}export function coerceField(e,t){if(void 0!==t&&""!==t)switch(e.type){case"f32":case"f64":case"i8":case"i16":case"i32":case"i64":case"u8":case"u16":case"u32":case"u64":{const e=Number(t);return Number.isFinite(e)?e:void 0}case"bool":return"true"===t||"1"===t;default:return t}}export function fieldLabel(e){const t=e.label??e.name,i=e.required?" *":"";return e.units?`${t} [${e.units}]${i}`:`${t}${i}`}export const SchemaFieldInput=({field:e,value:t,onChange:i,style:o})=>{const r=fieldChoices(e);return r?_jsx(Dropdown,{value:t??"",options:r,onChange:e=>i(e.value),placeholder:"—",style:o}):_jsx(InputText,{value:t??"",keyfilter:isNumericField(e)?"num":void 0,placeholder:e.description??void 0,onChange:e=>i(e.target.value),style:o})};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.6.8",
3
+ "version": "3.6.10",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -39,20 +39,15 @@ import { TabPanel, TabView } from 'primereact/tabview';
39
39
  import { EventEmitterContext } from '../../core/EventEmitterContext';
40
40
  import { MessageType } from '../../hub/CommandMessage';
41
41
  import { useAms, type AmsRole } from './AmsProvider';
42
+ import {
43
+ type SchemaField, SchemaFieldInput, coerceField, fieldLabel,
44
+ } from './schemaField';
42
45
 
43
46
  // Sentinel: keeps the Role dropdown consistent with the Add dialog's
44
47
  // "Other..." escape hatch so operators can move an asset to a custom
45
48
  // role that isn't declared in project.json.
46
49
  const ROLE_OTHER = '__other__';
47
50
 
48
- interface SchemaField {
49
- name: string;
50
- type: string;
51
- units?: string;
52
- label?: string;
53
- description?: string;
54
- required?: boolean;
55
- }
56
51
 
57
52
  interface SubLocationsSchema {
58
53
  label?: string;
@@ -104,24 +99,6 @@ function perAxisCalibrationFieldsFor(
104
99
  return (sub?.calibration_fields ?? []) as SchemaField[];
105
100
  }
106
101
 
107
- /** Coerce a per-cell string back to the field's declared type.
108
- * Mirrors the Add dialog's helper so empty strings drop out, numbers
109
- * come back as JSON numbers, bools as bools. */
110
- function coerceField(field: SchemaField, raw: string): any {
111
- if (raw === undefined || raw === '') return undefined;
112
- switch (field.type) {
113
- case 'f32': case 'f64':
114
- case 'i8': case 'i16': case 'i32': case 'i64':
115
- case 'u8': case 'u16': case 'u32': case 'u64': {
116
- const n = Number(raw);
117
- return Number.isFinite(n) ? n : undefined;
118
- }
119
- case 'bool':
120
- return raw === 'true' || raw === '1';
121
- default:
122
- return raw;
123
- }
124
- }
125
102
 
126
103
  /** Default a field's input from whatever's in the asset's custom blob.
127
104
  * Numbers / bools serialize back to strings for the text inputs to
@@ -551,38 +528,18 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
551
528
 
552
529
  {/* Schema-declared nameplate fields. */}
553
530
  {fields.map(field => {
554
- const label = field.label ?? field.name;
555
- const fullLabel = field.units
556
- ? `${label} [${field.units}]${field.required ? ' *' : ''}`
557
- : `${label}${field.required ? ' *' : ''}`;
558
- const isNum = field.type !== 'string' && field.type !== 'bool';
559
531
  return (
560
532
  <React.Fragment key={field.name}>
561
533
  <label title={field.description ?? undefined}>
562
- {fullLabel}
534
+ {fieldLabel(field)}
563
535
  </label>
564
- {field.type === 'bool' ? (
565
- <Dropdown
566
- value={customFields[field.name] ?? ''}
567
- options={[
568
- { label: 'true', value: 'true' },
569
- { label: 'false', value: 'false' },
570
- ]}
571
- onChange={(e) => setCustomFields(s => ({
572
- ...s, [field.name]: e.value,
573
- }))}
574
- placeholder="—"
575
- />
576
- ) : (
577
- <InputText
578
- value={customFields[field.name] ?? ''}
579
- keyfilter={isNum ? 'num' : undefined}
580
- placeholder={field.description ?? undefined}
581
- onChange={(e) => setCustomFields(s => ({
582
- ...s, [field.name]: e.target.value,
583
- }))}
584
- />
585
- )}
536
+ <SchemaFieldInput
537
+ field={field}
538
+ value={customFields[field.name] ?? ''}
539
+ onChange={(v) => setCustomFields(s => ({
540
+ ...s, [field.name]: v,
541
+ }))}
542
+ />
586
543
  </React.Fragment>
587
544
  );
588
545
  })}
@@ -625,16 +582,13 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
625
582
  {subLocationsSchema.fields.map(field => {
626
583
  const cellValue =
627
584
  subLocationFields[key]?.[field.name] ?? '';
628
- const isNum =
629
- field.type !== 'string' && field.type !== 'bool';
630
585
  return (
631
586
  <td key={field.name}
632
587
  style={{ padding: '0.25rem 0.5rem' }}>
633
- <InputText
588
+ <SchemaFieldInput
589
+ field={field}
634
590
  value={cellValue}
635
- keyfilter={isNum ? 'num' : undefined}
636
- onChange={(e) => {
637
- const v = e.target.value;
591
+ onChange={(v) => {
638
592
  setSubLocationFields(s => ({
639
593
  ...s,
640
594
  [key]: {
@@ -742,16 +696,13 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
742
696
  {perAxisCalibrationFields.map(field => {
743
697
  const cellValue =
744
698
  calPerAxisValues[key]?.[field.name] ?? '';
745
- const isNum =
746
- field.type !== 'string' && field.type !== 'bool';
747
699
  return (
748
700
  <td key={field.name}
749
701
  style={{ padding: '0.25rem 0.5rem' }}>
750
- <InputText
702
+ <SchemaFieldInput
703
+ field={field}
751
704
  value={cellValue}
752
- keyfilter={isNum ? 'num' : undefined}
753
- onChange={(e) => {
754
- const v = e.target.value;
705
+ onChange={(v) => {
755
706
  setCalPerAxisValues(s => ({
756
707
  ...s,
757
708
  [key]: {
@@ -776,22 +727,16 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
776
727
  gap: '0.5rem 1rem',
777
728
  alignItems: 'center' }}>
778
729
  {calibrationFields.map(field => {
779
- const label = field.label ?? field.name;
780
- const fullLabel = field.units
781
- ? `${label} [${field.units}]${field.required ? ' *' : ''}`
782
- : `${label}${field.required ? ' *' : ''}`;
783
- const isNum = field.type !== 'string'
784
- && field.type !== 'bool';
785
730
  return (
786
731
  <React.Fragment key={field.name}>
787
732
  <label title={field.description ?? undefined}>
788
- {fullLabel}
733
+ {fieldLabel(field)}
789
734
  </label>
790
- <InputText
735
+ <SchemaFieldInput
736
+ field={field}
791
737
  value={calValues[field.name] ?? ''}
792
- keyfilter={isNum ? 'num' : undefined}
793
- onChange={(e) => setCalValues(s => ({
794
- ...s, [field.name]: e.target.value,
738
+ onChange={(v) => setCalValues(s => ({
739
+ ...s, [field.name]: v,
795
740
  }))}
796
741
  />
797
742
  </React.Fragment>
@@ -15,6 +15,9 @@ import { Dialog } from 'primereact/dialog';
15
15
  import { EventEmitterContext } from '../../core/EventEmitterContext';
16
16
  import { MessageType } from '../../hub/CommandMessage';
17
17
  import { useAms, type AmsAssetEntry, type AmsRole } from './AmsProvider';
18
+ import {
19
+ type SchemaField, SchemaFieldInput, coerceField, fieldLabel,
20
+ } from './schemaField';
18
21
  import './AssetRegistryTable.css';
19
22
 
20
23
  // Sentinel value for the "Other..." dropdown option, which lets the
@@ -76,14 +79,6 @@ function roleUsageSummary(r: AmsRole): string {
76
79
  /** Render-ready descriptor for one schema-declared field. Schemas come
77
80
  * back from `ams.list_schemas` as untyped JSON; this narrows the
78
81
  * fields we actually care about for the Add dialog. */
79
- interface SchemaField {
80
- name: string;
81
- type: string;
82
- units?: string;
83
- label?: string;
84
- description?: string;
85
- required?: boolean;
86
- }
87
82
 
88
83
  /** The keyed-fields sub_locations schema declared on an asset_type
89
84
  * (multi-axis transducers etc.). `keys` fixes the row set; `fields`
@@ -138,24 +133,6 @@ function seedCustomFromRoleDefaults(
138
133
  return out;
139
134
  }
140
135
 
141
- /** Coerce a per-cell string from the matrix input back to its declared
142
- * type. Numeric strings → JSON numbers; booleans → bool; everything
143
- * else stays as a string. Matches the top-level fields coercion. */
144
- function coerceField(field: SchemaField, raw: string): any {
145
- if (raw === undefined || raw === '') return undefined;
146
- switch (field.type) {
147
- case 'f32': case 'f64':
148
- case 'i8': case 'i16': case 'i32': case 'i64':
149
- case 'u8': case 'u16': case 'u32': case 'u64': {
150
- const n = Number(raw);
151
- return Number.isFinite(n) ? n : undefined;
152
- }
153
- case 'bool':
154
- return raw === 'true' || raw === '1';
155
- default:
156
- return raw;
157
- }
158
- }
159
136
 
160
137
  export const AssetRegistryTable: React.FC = () => {
161
138
  const { schemas, roles, assets, refreshAssets, setSelection, selection } = useAms();
@@ -559,44 +536,21 @@ export const AssetRegistryTable: React.FC = () => {
559
536
  are what the placeholder resolver reads to seed
560
537
  NI bridge channels and EL3356 SDOs. Required
561
538
  fields gate Create. */}
562
- {fieldsForType.map(field => {
563
- const label = field.label ?? field.name;
564
- const fullLabel = field.units
565
- ? `${label} [${field.units}]${field.required ? ' *' : ''}`
566
- : `${label}${field.required ? ' *' : ''}`;
567
- const isNum = field.type !== 'string' && field.type !== 'bool';
568
- return (
569
- <React.Fragment key={field.name}>
570
- <label title={field.description ?? undefined}>
571
- {fullLabel}
572
- </label>
573
- {field.type === 'bool' ? (
574
- <Dropdown
575
- value={addState.customFields[field.name] ?? ''}
576
- options={[
577
- { label: 'true', value: 'true' },
578
- { label: 'false', value: 'false' },
579
- ]}
580
- onChange={(e) => setAddState(s => ({
581
- ...s,
582
- customFields: { ...s.customFields, [field.name]: e.value },
583
- }))}
584
- placeholder="—"
585
- />
586
- ) : (
587
- <InputText
588
- value={addState.customFields[field.name] ?? ''}
589
- keyfilter={isNum ? 'num' : undefined}
590
- placeholder={field.description ?? undefined}
591
- onChange={(e) => setAddState(s => ({
592
- ...s,
593
- customFields: { ...s.customFields, [field.name]: e.target.value },
594
- }))}
595
- />
596
- )}
597
- </React.Fragment>
598
- );
599
- })}
539
+ {fieldsForType.map(field => (
540
+ <React.Fragment key={field.name}>
541
+ <label title={field.description ?? undefined}>
542
+ {fieldLabel(field)}
543
+ </label>
544
+ <SchemaFieldInput
545
+ field={field}
546
+ value={addState.customFields[field.name] ?? ''}
547
+ onChange={(v) => setAddState(s => ({
548
+ ...s,
549
+ customFields: { ...s.customFields, [field.name]: v },
550
+ }))}
551
+ />
552
+ </React.Fragment>
553
+ ))}
600
554
  </div>
601
555
 
602
556
  {/* Per-axis matrix for asset types with a keyed-fields
@@ -643,16 +597,13 @@ export const AssetRegistryTable: React.FC = () => {
643
597
  {subLocationsSchema.fields.map(field => {
644
598
  const cellValue =
645
599
  addState.subLocationFields[key]?.[field.name] ?? '';
646
- const isNum =
647
- field.type !== 'string' && field.type !== 'bool';
648
600
  return (
649
601
  <td key={field.name}
650
602
  style={{ padding: '0.25rem 0.5rem' }}>
651
- <InputText
603
+ <SchemaFieldInput
604
+ field={field}
652
605
  value={cellValue}
653
- keyfilter={isNum ? 'num' : undefined}
654
- onChange={(e) => {
655
- const v = e.target.value;
606
+ onChange={(v) => {
656
607
  setAddState(s => ({
657
608
  ...s,
658
609
  subLocationFields: {
@@ -19,5 +19,10 @@ export { MissingAssetsBanner } from './MissingAssetsBanner';
19
19
  export { InstalledAssetPicker } from './InstalledAssetPicker';
20
20
  export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
21
21
  export { AssetNameplateGrid } from './AssetNameplateGrid';
22
+ // One schema field: its shape, how a form value is coerced back to its
23
+ // declared type, and the input control that edits it. Shared so a field can
24
+ // gain a behaviour (a dropdown, say) in one place rather than in each form.
25
+ export { SchemaFieldInput, coerceField, fieldChoices, fieldLabel, isNumericField } from './schemaField';
26
+ export type { SchemaField, FieldOption, Choice, SchemaFieldInputProps } from './schemaField';
22
27
  export { useInstalledAsset } from './useInstalledAsset';
23
28
  export type { InstalledAssetState } from './useInstalledAsset';
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @file
3
+ * One asset-schema field: its shape, how a typed value is coerced out of the
4
+ * string the form holds, and the input control that edits it.
5
+ *
6
+ * Every AMS form renders the same thing in seven places — the asset create
7
+ * form, the asset edit form, the per-axis sub-location matrix in each of those,
8
+ * the per-axis calibration matrix, the flat calibration values, and the
9
+ * standalone calibration dialog. They had drifted into seven copies of the same
10
+ * `label ?? name` / `[units]` / `*` / bool-Dropdown-else-InputText block, plus
11
+ * three copies of `SchemaField` and two of `coerceField`.
12
+ *
13
+ * That duplication is why a field could not gain a behaviour: adding one meant
14
+ * finding all seven. `capacity_units` is the case that forced this — it is a
15
+ * free-text string today, so an operator typed "Newtons" where the consuming
16
+ * machine matched on "N", and the fix is a dropdown in every one of those
17
+ * places or it is not a fix.
18
+ */
19
+
20
+ import React from 'react';
21
+ import { Dropdown } from 'primereact/dropdown';
22
+ import { InputText } from 'primereact/inputtext';
23
+
24
+ /**
25
+ * One choice in a field's dropdown, as authored in the asset-type schema.
26
+ *
27
+ * Mirrors `FieldOption` in autocore-ams's `config.rs`: either a bare scalar,
28
+ * where the stored value and the operator-facing label are the same, or an
29
+ * explicit pair when they should differ.
30
+ *
31
+ * ```jsonc
32
+ * "options": ["N", "kN", "lbf"]
33
+ * "options": [{ "label": "Newtons (N)", "value": "N" }]
34
+ * ```
35
+ */
36
+ export type FieldOption =
37
+ | string
38
+ | number
39
+ | boolean
40
+ | { label: string; value: unknown };
41
+
42
+ /** One field of an asset type's `fields` / `calibration_fields` schema. */
43
+ export interface SchemaField {
44
+ name: string;
45
+ type: string;
46
+ units?: string;
47
+ label?: string;
48
+ description?: string;
49
+ required?: boolean;
50
+ /** Fixed set of choices. When present the field is a dropdown, not a
51
+ * free-form input — see the file header. */
52
+ options?: FieldOption[];
53
+ }
54
+
55
+ /** A choice normalised for PrimeReact's `Dropdown`. */
56
+ export interface Choice {
57
+ label: string;
58
+ value: string;
59
+ }
60
+
61
+ /**
62
+ * The choices this field offers, or `null` if it is free-form.
63
+ *
64
+ * Booleans are a fixed two-choice dropdown, as they always were. An explicit
65
+ * `options` list wins over the type's own rendering, which is what lets a
66
+ * `string` field be constrained without inventing a new type.
67
+ *
68
+ * Values are carried as STRINGS because that is what the form state holds;
69
+ * `coerceField` converts back on submit.
70
+ */
71
+ export function fieldChoices(field: SchemaField): Choice[] | null {
72
+ if (field.options && field.options.length > 0) {
73
+ return field.options.map((opt) =>
74
+ opt !== null && typeof opt === 'object' && 'value' in opt
75
+ ? { label: String(opt.label), value: String(opt.value) }
76
+ : { label: String(opt), value: String(opt) });
77
+ }
78
+ if (field.type === 'bool') {
79
+ return [
80
+ { label: 'true', value: 'true' },
81
+ { label: 'false', value: 'false' },
82
+ ];
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /** True when the field takes digits only, so the input can keyfilter. A field
88
+ * with `options` is never free-typed, so it never keyfilters. */
89
+ export function isNumericField(field: SchemaField): boolean {
90
+ if (fieldChoices(field)) return false;
91
+ return field.type !== 'string' && field.type !== 'bool';
92
+ }
93
+
94
+ /**
95
+ * Coerce the form's string back to the field's declared type.
96
+ *
97
+ * Numeric strings become JSON numbers and booleans become bools, because the
98
+ * placeholder resolver and the SDO writers downstream expect native types — a
99
+ * string `"5000"` does not satisfy a `${ams.*}` reference asking for a numeric
100
+ * `max_val`. Empty stays `undefined` so an untouched field is omitted from the
101
+ * record rather than stored blank.
102
+ */
103
+ export function coerceField(field: SchemaField, raw: string | undefined): any {
104
+ if (raw === undefined || raw === '') return undefined;
105
+ switch (field.type) {
106
+ case 'f32': case 'f64':
107
+ case 'i8': case 'i16': case 'i32': case 'i64':
108
+ case 'u8': case 'u16': case 'u32': case 'u64': {
109
+ const n = Number(raw);
110
+ return Number.isFinite(n) ? n : undefined;
111
+ }
112
+ case 'bool':
113
+ return raw === 'true' || raw === '1';
114
+ default:
115
+ return raw;
116
+ }
117
+ }
118
+
119
+ /** The label a form shows for this field: pretty name, units, required mark. */
120
+ export function fieldLabel(field: SchemaField): string {
121
+ const base = field.label ?? field.name;
122
+ const req = field.required ? ' *' : '';
123
+ return field.units ? `${base} [${field.units}]${req}` : `${base}${req}`;
124
+ }
125
+
126
+ export interface SchemaFieldInputProps {
127
+ field: SchemaField;
128
+ /** Current value, as the string the form state holds. */
129
+ value: string;
130
+ /** Called with the new string. Coercion happens on submit, not here. */
131
+ onChange: (next: string) => void;
132
+ style?: React.CSSProperties;
133
+ }
134
+
135
+ /**
136
+ * The input control for one schema field: a dropdown when the field has a
137
+ * fixed set of choices, a text box otherwise.
138
+ *
139
+ * Deliberately uncontrolled about layout — callers place it in a grid or a
140
+ * table cell — so this owns only the decision that must be the same everywhere.
141
+ */
142
+ export const SchemaFieldInput: React.FC<SchemaFieldInputProps> = ({
143
+ field, value, onChange, style,
144
+ }) => {
145
+ const choices = fieldChoices(field);
146
+ if (choices) {
147
+ return (
148
+ <Dropdown
149
+ value={value ?? ''}
150
+ options={choices}
151
+ onChange={(e) => onChange(e.value)}
152
+ placeholder="—"
153
+ style={style}
154
+ />
155
+ );
156
+ }
157
+ return (
158
+ <InputText
159
+ value={value ?? ''}
160
+ keyfilter={isNumericField(field) ? 'num' : undefined}
161
+ placeholder={field.description ?? undefined}
162
+ onChange={(e) => onChange(e.target.value)}
163
+ style={style}
164
+ />
165
+ );
166
+ };