@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.
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
- package/dist/components/ams/AssetEditDialog.js +1 -1
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/ams/index.d.ts +2 -0
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/dist/components/ams/schemaField.d.ts +95 -0
- package/dist/components/ams/schemaField.d.ts.map +1 -0
- package/dist/components/ams/schemaField.js +1 -0
- package/package.json +1 -1
- package/src/components/ams/AssetEditDialog.tsx +22 -77
- package/src/components/ams/AssetRegistryTable.tsx +21 -70
- package/src/components/ams/index.ts +5 -0
- package/src/components/ams/schemaField.tsx +166 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetEditDialog.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetEditDialog.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAmD,MAAM,OAAO,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;
|
|
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;
|
|
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
|
@@ -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
|
-
{
|
|
534
|
+
{fieldLabel(field)}
|
|
563
535
|
</label>
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
<
|
|
588
|
+
<SchemaFieldInput
|
|
589
|
+
field={field}
|
|
634
590
|
value={cellValue}
|
|
635
|
-
|
|
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
|
-
<
|
|
702
|
+
<SchemaFieldInput
|
|
703
|
+
field={field}
|
|
751
704
|
value={cellValue}
|
|
752
|
-
|
|
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
|
-
{
|
|
733
|
+
{fieldLabel(field)}
|
|
789
734
|
</label>
|
|
790
|
-
<
|
|
735
|
+
<SchemaFieldInput
|
|
736
|
+
field={field}
|
|
791
737
|
value={calValues[field.name] ?? ''}
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
<
|
|
603
|
+
<SchemaFieldInput
|
|
604
|
+
field={field}
|
|
652
605
|
value={cellValue}
|
|
653
|
-
|
|
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
|
+
};
|