@adcops/autocore-react 3.3.101 → 3.3.105

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":"TisConfigEditor.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/TisConfigEditor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM5E,OAAO,uBAAuB,CAAC;AAE/B,MAAM,WAAW,oBAAoB;IACjC;+EAC2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAkBD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAoQ1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"TisConfigEditor.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/TisConfigEditor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM5E,OAAO,uBAAuB,CAAC;AAE/B,MAAM,WAAW,oBAAoB;IACjC;+EAC2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAkBD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAyQ1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import{useEffect,useMemo,useState}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{useContext}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTisConfig}from"../../hooks/useTisConfig";import{useAmsAssetTypes}from"../../hooks/useAmsAssetTypes";import{MethodFormEditor}from"./editor/MethodFormEditor";import{SaveDiffDialog}from"./editor/SaveDiffDialog";import"./TisConfigEditor.css";const EMPTY_METHOD={label:"",description:"",project_fields:[],config_fields:[],cycle_fields:[],results_fields:[],views:{},asset_refs:[]};export const TisConfigEditor=({projectId:e,invoker:t})=>{const s=useContext(EventEmitterContext),i=t??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),o=useTisConfig(e,{invoker:i}),a=useAmsAssetTypes({invoker:i}),[n,r]=useState(null),[l,d]=useState(null),[c,m]=useState(!1),[f,p]=useState(!1),[h,u]=useState(!1),[_,g]=useState(""),x=useMemo(()=>o.config?Object.entries(o.config.methods).map(([e,t])=>({id:e,label:t?.label??""})):[],[o.config]);useEffect(()=>{if(!n&&o.config){const e=o.config.defaultMethodId||Object.keys(o.config.methods)[0]||null;r(e)}},[o.config,n]),useEffect(()=>{d(null)},[n]);return _jsxs("div",{className:"tis-editor",children:[_jsxs("header",{className:"tis-editor__header",children:[_jsxs("h2",{children:["Test Methods"," ",o.config?.dirty&&_jsx("span",{className:"tis-editor__dirty-pill",children:"unsaved"})]}),_jsxs("div",{className:"tis-editor__header-actions",children:[_jsx(Button,{label:"Save…",icon:"pi pi-save",disabled:c||!o.config?.dirty,onClick:()=>p(!0)}),_jsx(Button,{label:"Revert",icon:"pi pi-undo",className:"p-button-secondary",disabled:c||!o.config?.dirty,onClick:async()=>{if(window.confirm("Discard all in-progress edits? This cannot be undone.")){m(!0);try{await o.revert()}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),o.error&&_jsxs("div",{className:"tis-editor__error",children:[_jsx("strong",{children:"Error:"})," ",_jsx("pre",{children:o.error})]}),_jsxs("div",{className:"tis-editor__body",children:[_jsxs("aside",{className:"tis-editor__sidebar",children:[_jsxs("div",{className:"tis-editor__sidebar-actions",children:[_jsx(Button,{label:"New",icon:"pi pi-plus",disabled:c,onClick:()=>u(!0)}),_jsx(Button,{label:"Duplicate",icon:"pi pi-clone",className:"p-button-secondary",disabled:c||!n,onClick:async()=>{if(!n||!o.config)return;const e=o.config.methods[n];if(!e)return;let t=`${n}_copy`,s=2;for(;o.config.methods[t];)t=`${n}_copy_${s++}`;m(!0);try{await o.putMethod(t,JSON.parse(JSON.stringify(e))),r(t)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}),_jsx(Button,{label:"Delete",icon:"pi pi-trash",className:"p-button-danger",disabled:c||!n,onClick:async()=>{if(n&&window.confirm(`Remove method "${n}"? This is staged — Save persists it.`)){m(!0);try{await o.removeMethod(n),r(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]}),_jsxs(DataTable,{value:x,selection:x.find(e=>e.id===n)??null,onSelectionChange:e=>r(e.value?.id??null),selectionMode:"single",dataKey:"id",scrollable:!0,scrollHeight:"flex",emptyMessage:o.loading?"Loading…":"No test methods defined.",children:[_jsx(Column,{field:"id",header:"Method ID"}),_jsx(Column,{field:"label",header:"Label"})]})]}),_jsx("section",{className:"tis-editor__detail",children:n&&o.config?.methods[n]?_jsxs(_Fragment,{children:[l&&_jsx("div",{className:"tis-editor__error",children:_jsx("pre",{children:l})}),_jsx(MethodFormEditor,{methodId:n,method:o.config.methods[n],onApply:async e=>{if(n){m(!0);try{await o.putMethod(n,e),d(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}},busy:c,knownAssetTypes:a.types})]}):_jsx("div",{className:"tis-editor__empty",children:"Select a test method on the left, or create a new one."})})]}),_jsxs(Dialog,{header:"New Test Method",visible:h,onHide:()=>u(!1),style:{width:"24rem"},children:[_jsxs("label",{className:"tis-editor__new-method-label",children:["Method ID",_jsx(InputText,{value:_,onChange:e=>g(e.target.value),placeholder:"e.g. translational_traction",autoFocus:!0})]}),_jsx("small",{children:"Canonical key — appears in wire payloads, on-disk paths, and generated code."}),_jsxs("div",{style:{display:"flex",gap:"0.5rem",justifyContent:"flex-end",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:()=>u(!1)}),_jsx(Button,{label:"Create",disabled:!_.trim()||c,onClick:async()=>{const e=_.trim();if(e)if(o.config?.methods[e])d(`A method named "${e}" already exists.`);else{m(!0);try{await o.putMethod(e,EMPTY_METHOD),r(e),u(!1),g("")}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),_jsx(SaveDiffDialog,{visible:f,staged:o.config?.methods??{},invoker:i,onConfirm:async()=>{m(!0);try{await o.save(),p(!1)}catch(e){d(String(e?.message??e))}finally{m(!1)}},onCancel:()=>p(!1)})]})};export default TisConfigEditor;
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import{useEffect,useMemo,useState}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{useContext}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTisConfig}from"../../hooks/useTisConfig";import{useAmsAssetTypes}from"../../hooks/useAmsAssetTypes";import{MethodFormEditor}from"./editor/MethodFormEditor";import{SaveDiffDialog}from"./editor/SaveDiffDialog";import"./TisConfigEditor.css";const EMPTY_METHOD={label:"",description:"",project_fields:[],config_fields:[],cycle_fields:[],results_fields:[],views:{},asset_refs:[]};export const TisConfigEditor=({projectId:e,invoker:t})=>{const s=useContext(EventEmitterContext),i=useMemo(()=>t??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),[t,s]),o=useTisConfig(e,{invoker:i}),a=useAmsAssetTypes({invoker:i}),[n,r]=useState(null),[l,d]=useState(null),[c,m]=useState(!1),[f,p]=useState(!1),[h,u]=useState(!1),[_,g]=useState(""),x=useMemo(()=>o.config?Object.entries(o.config.methods).map(([e,t])=>({id:e,label:t?.label??""})):[],[o.config]);useEffect(()=>{if(!n&&o.config){const e=o.config.defaultMethodId||Object.keys(o.config.methods)[0]||null;r(e)}},[o.config,n]),useEffect(()=>{d(null)},[n]);return _jsxs("div",{className:"tis-editor",children:[_jsxs("header",{className:"tis-editor__header",children:[_jsxs("h2",{children:["Test Methods"," ",o.config?.dirty&&_jsx("span",{className:"tis-editor__dirty-pill",children:"unsaved"})]}),_jsxs("div",{className:"tis-editor__header-actions",children:[_jsx(Button,{label:"Save…",icon:"pi pi-save",disabled:c||!o.config?.dirty,onClick:()=>p(!0)}),_jsx(Button,{label:"Revert",icon:"pi pi-undo",className:"p-button-secondary",disabled:c||!o.config?.dirty,onClick:async()=>{if(window.confirm("Discard all in-progress edits? This cannot be undone.")){m(!0);try{await o.revert()}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),o.error&&_jsxs("div",{className:"tis-editor__error",children:[_jsx("strong",{children:"Error:"})," ",_jsx("pre",{children:o.error})]}),_jsxs("div",{className:"tis-editor__body",children:[_jsxs("aside",{className:"tis-editor__sidebar",children:[_jsxs("div",{className:"tis-editor__sidebar-actions",children:[_jsx(Button,{label:"New",icon:"pi pi-plus",disabled:c,onClick:()=>u(!0)}),_jsx(Button,{label:"Duplicate",icon:"pi pi-clone",className:"p-button-secondary",disabled:c||!n,onClick:async()=>{if(!n||!o.config)return;const e=o.config.methods[n];if(!e)return;let t=`${n}_copy`,s=2;for(;o.config.methods[t];)t=`${n}_copy_${s++}`;m(!0);try{await o.putMethod(t,JSON.parse(JSON.stringify(e))),r(t)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}),_jsx(Button,{label:"Delete",icon:"pi pi-trash",className:"p-button-danger",disabled:c||!n,onClick:async()=>{if(n&&window.confirm(`Remove method "${n}"? This is staged — Save persists it.`)){m(!0);try{await o.removeMethod(n),r(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]}),_jsxs(DataTable,{value:x,selection:x.find(e=>e.id===n)??null,onSelectionChange:e=>r(e.value?.id??null),selectionMode:"single",dataKey:"id",scrollable:!0,scrollHeight:"flex",emptyMessage:o.loading?"Loading…":"No test methods defined.",children:[_jsx(Column,{field:"id",header:"Method ID"}),_jsx(Column,{field:"label",header:"Label"})]})]}),_jsx("section",{className:"tis-editor__detail",children:n&&o.config?.methods[n]?_jsxs(_Fragment,{children:[l&&_jsx("div",{className:"tis-editor__error",children:_jsx("pre",{children:l})}),_jsx(MethodFormEditor,{methodId:n,method:o.config.methods[n],onApply:async e=>{if(n){m(!0);try{await o.putMethod(n,e),d(null)}catch(e){d(String(e?.message??e))}finally{m(!1)}}},busy:c,knownAssetTypes:a.types})]}):_jsx("div",{className:"tis-editor__empty",children:"Select a test method on the left, or create a new one."})})]}),_jsxs(Dialog,{header:"New Test Method",visible:h,onHide:()=>u(!1),style:{width:"24rem"},children:[_jsxs("label",{className:"tis-editor__new-method-label",children:["Method ID",_jsx(InputText,{value:_,onChange:e=>g(e.target.value),placeholder:"e.g. translational_traction",autoFocus:!0})]}),_jsx("small",{children:"Canonical key — appears in wire payloads, on-disk paths, and generated code."}),_jsxs("div",{style:{display:"flex",gap:"0.5rem",justifyContent:"flex-end",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:()=>u(!1)}),_jsx(Button,{label:"Create",disabled:!_.trim()||c,onClick:async()=>{const e=_.trim();if(e)if(o.config?.methods[e])d(`A method named "${e}" already exists.`);else{m(!0);try{await o.putMethod(e,EMPTY_METHOD),r(e),u(!1),g("")}catch(e){d(String(e?.message??e))}finally{m(!1)}}}})]})]}),_jsx(SaveDiffDialog,{visible:f,staged:o.config?.methods??{},invoker:i,onConfirm:async()=>{m(!0);try{await o.save(),p(!1)}catch(e){d(String(e?.message??e))}finally{m(!1)}},onCancel:()=>p(!1)})]})};export default TisConfigEditor;
@@ -1 +1 @@
1
- {"version":3,"file":"useTisConfig.d.ts","sourceRoot":"","sources":["../../src/hooks/useTisConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH;8EAC8E;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;wFACwF;AACxF,MAAM,WAAW,aAAa;IAC1B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QACtC,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,aAAa,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;CACN;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,wBAAgB,YAAY,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC9B,kBAAkB,CAqGpB"}
1
+ {"version":3,"file":"useTisConfig.d.ts","sourceRoot":"","sources":["../../src/hooks/useTisConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH;8EAC8E;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;wFACwF;AACxF,MAAM,WAAW,aAAa;IAC1B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QACtC,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,aAAa,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;CACN;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,wBAAgB,YAAY,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC9B,kBAAkB,CAgHpB"}
@@ -1 +1 @@
1
- import{useCallback,useContext,useEffect,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export function useTisConfig(e,t){const s=useContext(EventEmitterContext),o=t?.invoker??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),[a,r]=useState(null),[i,n]=useState(!0),[c,d]=useState(null),l=useCallback(async()=>{n(!0);try{const t=await o("tis.show_config",{project_id:e});if(!t.success)return d(t.error_message??"tis.show_config failed"),void r(null);const s=t.data??{};r({projectId:e,methods:s.test_methods??{},dirty:!!s.dirty,defaultMethodId:"string"==typeof s.default_method_id?s.default_method_id:""}),d(null)}catch(e){d(String(e?.message??e)),r(null)}finally{n(!1)}},[e,o]);useEffect(()=>{l()},[l]);const u=useCallback(async(t,s)=>{const a=await o("tis.put_method",{project_id:e,method_id:t,method:s});if(!a.success){const e=a.error_message??"tis.put_method failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),f=useCallback(async t=>{const s=await o("tis.remove_method",{project_id:e,method_id:t});if(!s.success){const e=s.error_message??"tis.remove_method failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),m=useCallback(async()=>{const t=await o("tis.save_config",{project_id:e});if(!t.success){const e=t.error_message??"tis.save_config failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]),_=useCallback(async()=>{const t=await o("tis.discard_config_changes",{project_id:e});if(!t.success){const e=t.error_message??"tis.discard_config_changes failed";throw d(e),new Error(e)}d(null),await l()},[o,e,l]);return{config:a,loading:i,error:c,refresh:l,putMethod:u,removeMethod:f,save:m,revert:_}}
1
+ import{useCallback,useContext,useEffect,useRef,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export function useTisConfig(e,t){const s=useContext(EventEmitterContext),r=t?.invoker??(async(e,t)=>await s.invoke(e,MessageType.Request,t)),o=useRef(r);o.current=r;const[a,n]=useState(null),[i,c]=useState(!0),[u,d]=useState(null),l=useCallback(async()=>{c(!0);try{const t=await o.current("tis.show_config",{project_id:e});if(!t.success)return d(t.error_message??"tis.show_config failed"),void n(null);const s=t.data??{};n({projectId:e,methods:s.test_methods??{},dirty:!!s.dirty,defaultMethodId:"string"==typeof s.default_method_id?s.default_method_id:""}),d(null)}catch(e){d(String(e?.message??e)),n(null)}finally{c(!1)}},[e]);useEffect(()=>{l()},[l]);const f=useCallback(async(t,s)=>{const r=await o.current("tis.put_method",{project_id:e,method_id:t,method:s});if(!r.success){const e=r.error_message??"tis.put_method failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),m=useCallback(async t=>{const s=await o.current("tis.remove_method",{project_id:e,method_id:t});if(!s.success){const e=s.error_message??"tis.remove_method failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),_=useCallback(async()=>{const t=await o.current("tis.save_config",{project_id:e});if(!t.success){const e=t.error_message??"tis.save_config failed";throw d(e),new Error(e)}d(null),await l()},[e,l]),h=useCallback(async()=>{const t=await o.current("tis.discard_config_changes",{project_id:e});if(!t.success){const e=t.error_message??"tis.discard_config_changes failed";throw d(e),new Error(e)}d(null),await l()},[e,l]);return{config:a,loading:i,error:u,refresh:l,putMethod:f,removeMethod:m,save:_,revert:h}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.101",
3
+ "version": "3.3.105",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -58,10 +58,15 @@ const EMPTY_METHOD: TestMethod = {
58
58
 
59
59
  export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, invoker }) => {
60
60
  const ctx = useContext(EventEmitterContext);
61
- // Resolve invoker once so it can be passed into the SaveDiffDialog
62
- // alongside the hook's internal use of it.
63
- const effectiveInvoker: TisIpcInvoker = invoker
64
- ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
61
+ // Resolve the invoker ONCE and memoize it so it keeps a stable identity
62
+ // across renders. Both useTisConfig and SaveDiffDialog key effects off
63
+ // this invoker; an invoker rebuilt every render drove a refetch loop in
64
+ // the hook that continuously reset the editor draft and wiped edits.
65
+ const effectiveInvoker: TisIpcInvoker = useMemo(
66
+ () => invoker
67
+ ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any)),
68
+ [invoker, ctx],
69
+ );
65
70
 
66
71
  const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
67
72
  const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
@@ -16,7 +16,7 @@
16
16
  * demo and by tests). When omitted, the hook reads `EventEmitterContext`.
17
17
  */
18
18
 
19
- import { useCallback, useContext, useEffect, useState } from 'react';
19
+ import { useCallback, useContext, useEffect, useRef, useState } from 'react';
20
20
  import { EventEmitterContext } from '../core/EventEmitterContext';
21
21
  import { MessageType } from '../hub/CommandMessage';
22
22
 
@@ -70,6 +70,18 @@ export function useTisConfig(
70
70
  return await ctx.invoke(topic as any, MessageType.Request, payload as any);
71
71
  });
72
72
 
73
+ // Hold the invoker in a ref so the memoized callbacks below keep a STABLE
74
+ // identity even when the caller passes a freshly-built invoker on every
75
+ // render (the common case — TisConfigEditor does exactly that). Depending
76
+ // on `invoker` directly made `refresh` change every render, so the mount
77
+ // effect re-ran in an infinite refetch loop: each refetch replaced
78
+ // `config`, which reset MethodFormEditor's draft, silently wiping
79
+ // in-progress edits (deleted views reappeared, Apply never stayed
80
+ // enabled). The ref is refreshed each render so calls still use the latest
81
+ // invoker without destabilising the callbacks.
82
+ const invokerRef = useRef(invoker);
83
+ invokerRef.current = invoker;
84
+
73
85
  const [config, setConfig] = useState<TisConfig | null>(null);
74
86
  const [loading, setLoading] = useState<boolean>(true);
75
87
  const [error, setError] = useState<string | null>(null);
@@ -80,7 +92,7 @@ export function useTisConfig(
80
92
  const refresh = useCallback(async () => {
81
93
  setLoading(true);
82
94
  try {
83
- const resp = await invoker('tis.show_config', { project_id: projectId });
95
+ const resp = await invokerRef.current('tis.show_config', { project_id: projectId });
84
96
  if (!resp.success) {
85
97
  setError(resp.error_message ?? 'tis.show_config failed');
86
98
  setConfig(null);
@@ -102,15 +114,14 @@ export function useTisConfig(
102
114
  } finally {
103
115
  setLoading(false);
104
116
  }
105
- // eslint-disable-next-line react-hooks/exhaustive-deps
106
- }, [projectId, invoker]);
117
+ }, [projectId]);
107
118
 
108
119
  useEffect(() => {
109
120
  void refresh();
110
121
  }, [refresh]);
111
122
 
112
123
  const putMethod = useCallback(async (methodId: string, method: TestMethod) => {
113
- const resp = await invoker('tis.put_method', {
124
+ const resp = await invokerRef.current('tis.put_method', {
114
125
  project_id: projectId,
115
126
  method_id: methodId,
116
127
  method,
@@ -122,10 +133,10 @@ export function useTisConfig(
122
133
  }
123
134
  setError(null);
124
135
  await refresh();
125
- }, [invoker, projectId, refresh]);
136
+ }, [projectId, refresh]);
126
137
 
127
138
  const removeMethod = useCallback(async (methodId: string) => {
128
- const resp = await invoker('tis.remove_method', {
139
+ const resp = await invokerRef.current('tis.remove_method', {
129
140
  project_id: projectId,
130
141
  method_id: methodId,
131
142
  });
@@ -136,10 +147,10 @@ export function useTisConfig(
136
147
  }
137
148
  setError(null);
138
149
  await refresh();
139
- }, [invoker, projectId, refresh]);
150
+ }, [projectId, refresh]);
140
151
 
141
152
  const save = useCallback(async () => {
142
- const resp = await invoker('tis.save_config', { project_id: projectId });
153
+ const resp = await invokerRef.current('tis.save_config', { project_id: projectId });
143
154
  if (!resp.success) {
144
155
  const m = resp.error_message ?? 'tis.save_config failed';
145
156
  setError(m);
@@ -147,10 +158,10 @@ export function useTisConfig(
147
158
  }
148
159
  setError(null);
149
160
  await refresh();
150
- }, [invoker, projectId, refresh]);
161
+ }, [projectId, refresh]);
151
162
 
152
163
  const revert = useCallback(async () => {
153
- const resp = await invoker('tis.discard_config_changes', { project_id: projectId });
164
+ const resp = await invokerRef.current('tis.discard_config_changes', { project_id: projectId });
154
165
  if (!resp.success) {
155
166
  const m = resp.error_message ?? 'tis.discard_config_changes failed';
156
167
  setError(m);
@@ -158,7 +169,7 @@ export function useTisConfig(
158
169
  }
159
170
  setError(null);
160
171
  await refresh();
161
- }, [invoker, projectId, refresh]);
172
+ }, [projectId, refresh]);
162
173
 
163
174
  return { config, loading, error, refresh, putMethod, removeMethod, save, revert };
164
175
  }