@adcops/autocore-react 3.5.7 → 3.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/components/forms/useEditDraft.d.ts +49 -0
  2. package/dist/components/forms/useEditDraft.d.ts.map +1 -0
  3. package/dist/components/forms/useEditDraft.js +1 -0
  4. package/dist/components/index.d.ts +6 -0
  5. package/dist/components/index.d.ts.map +1 -1
  6. package/dist/components/index.js +1 -1
  7. package/dist/components/tis/TestDataView.d.ts +8 -0
  8. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  9. package/dist/components/tis/TestDataView.js +1 -1
  10. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  11. package/dist/components/tis/TestRawDataView.js +1 -1
  12. package/dist/components/tis/chartViews.d.ts +57 -0
  13. package/dist/components/tis/chartViews.d.ts.map +1 -0
  14. package/dist/components/tis/chartViews.js +1 -0
  15. package/dist/components/tis-editor/TisConfigEditor.css +11 -0
  16. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  17. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  18. package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
  19. package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
  20. package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
  21. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
  22. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
  23. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  24. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  25. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  26. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
  27. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
  28. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
  29. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  30. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  31. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  32. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  33. package/dist/components/tis-editor/types.d.ts +6 -0
  34. package/dist/components/tis-editor/types.d.ts.map +1 -1
  35. package/dist/components/varpick/VariableInput.d.ts +41 -0
  36. package/dist/components/varpick/VariableInput.d.ts.map +1 -0
  37. package/dist/components/varpick/VariableInput.js +1 -0
  38. package/dist/components/varpick/VariablePicker.d.ts +45 -0
  39. package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
  40. package/dist/components/varpick/VariablePicker.js +1 -0
  41. package/dist/components/varpick/index.d.ts +16 -0
  42. package/dist/components/varpick/index.d.ts.map +1 -0
  43. package/dist/components/varpick/index.js +1 -0
  44. package/dist/components/varpick/tisCatalogs.d.ts +85 -0
  45. package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
  46. package/dist/components/varpick/tisCatalogs.js +1 -0
  47. package/dist/components/varpick/varpick.css +167 -0
  48. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  49. package/dist/core/AutoCoreTagContext.js +1 -1
  50. package/dist/core/AutoCoreTagTypes.d.ts +13 -0
  51. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  52. package/dist/hooks/index.d.ts +4 -0
  53. package/dist/hooks/index.d.ts.map +1 -1
  54. package/dist/hooks/index.js +1 -1
  55. package/dist/hooks/useGmVariables.d.ts +57 -0
  56. package/dist/hooks/useGmVariables.d.ts.map +1 -0
  57. package/dist/hooks/useGmVariables.js +1 -0
  58. package/dist/hooks/useSequenceProducers.d.ts +38 -0
  59. package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
  60. package/dist/hooks/useSequenceProducers.js +1 -0
  61. package/package.json +7 -2
  62. package/src/components/forms/useEditDraft.ts +0 -0
  63. package/src/components/index.ts +20 -0
  64. package/src/components/tis/TestDataView.tsx +16 -10
  65. package/src/components/tis/TestRawDataView.tsx +6 -7
  66. package/src/components/tis/chartViews.ts +104 -0
  67. package/src/components/tis-editor/TisConfigEditor.css +11 -0
  68. package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
  69. package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
  70. package/src/components/tis-editor/editor/ChartViewDialog.tsx +48 -17
  71. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
  72. package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
  73. package/src/components/tis-editor/editor/TestFieldDialog.tsx +90 -23
  74. package/src/components/tis-editor/editor/ViewsEditor.tsx +56 -5
  75. package/src/components/tis-editor/types.ts +6 -0
  76. package/src/components/varpick/VariableInput.tsx +126 -0
  77. package/src/components/varpick/VariablePicker.tsx +168 -0
  78. package/src/components/varpick/index.ts +22 -0
  79. package/src/components/varpick/tisCatalogs.ts +235 -0
  80. package/src/components/varpick/varpick.css +167 -0
  81. package/src/core/AutoCoreTagContext.tsx +3 -1
  82. package/src/core/AutoCoreTagTypes.ts +14 -0
  83. package/src/hooks/index.ts +11 -0
  84. package/src/hooks/useGmVariables.ts +142 -0
  85. package/src/hooks/useSequenceProducers.ts +104 -0
  86. package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
  87. package/tools/tests/dist/autocore-react.css +257 -0
  88. package/tools/tests/dist/tis-editor.entry.js +9295 -0
  89. package/tools/tests/dist/varpick.entry.js +113 -0
  90. package/tools/tests/live-server.probe.mjs +172 -0
  91. package/tools/tests/tis-editor.entry.tsx +46 -0
  92. package/tools/tests/tis-editor.test.mjs +133 -0
  93. package/tools/tests/varpick.entry.tsx +81 -0
  94. package/tools/tests/varpick.test.mjs +208 -0
  95. package/tools/tests/vite.config.mjs +35 -0
@@ -1,4 +1,8 @@
1
1
  export { useAdsRegisterSymbols, useAdsWriteValue, useAdsTapValue } from "./adsHooks";
2
2
  export { useScaledValue, kMillimeters2Inches } from './useScaledValue';
3
3
  export * from './useServeletData';
4
+ export { useGmVariables } from './useGmVariables';
5
+ export type { UseGmVariablesResult, UseGmVariablesOptions, GmIpcInvoker } from './useGmVariables';
6
+ export { useSequenceProducers, collectStoredNames } from './useSequenceProducers';
7
+ export type { UseSequenceProducersResult, UseSequenceProducersOptions } from './useSequenceProducers';
4
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,cAAc,mBAAmB,CAAC;AAKlC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIlG,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClF,YAAY,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1 +1 @@
1
- export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";export*from"./useServeletData";
1
+ export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";export*from"./useServeletData";export{useGmVariables}from"./useGmVariables";export{useSequenceProducers,collectStoredNames}from"./useSequenceProducers";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * useGmVariables — the machine's GM variable table, for the variable picker.
3
+ *
4
+ * Reads `gm.get_variables`: `gm.get_catalog` with the metadata attached — type,
5
+ * description, quantity, and whether a UI may write the variable. Everything it
6
+ * returns already lives in project.json; the verb exists because a catalog of
7
+ * bare FQDN strings cannot tell an author what a name means, which is why every
8
+ * field that asks for one has been a text box.
9
+ *
10
+ * ## Degrading on an older server
11
+ *
12
+ * `gm.get_variables` was added alongside the picker, so a machine running an
13
+ * older `autocore_server` answers with an error. That is not a failure worth
14
+ * showing an operator: the picker still works, it just lists names without
15
+ * descriptions. So the hook falls back to the generated tag spec the HMI
16
+ * already has (`acTagSpec`), which carries FQDN, type and quantity but no prose,
17
+ * and reports the degradation through `partial` rather than through `error`.
18
+ *
19
+ * The fallback covers strictly less: `acctl codegen-tags` only emits variables
20
+ * marked `ux: true`, and a `${gm.…}` bound may legitimately name one that is
21
+ * not. Hence `partial` — a caller can say so on screen instead of quietly
22
+ * offering an incomplete list.
23
+ */
24
+ import type { GmVariable } from '@adcops/autocore-varpick';
25
+ import type { TagConfig } from '../core/AutoCoreTagTypes';
26
+ export interface GmIpcInvoker {
27
+ (topic: string, payload: object): Promise<{
28
+ success: boolean;
29
+ data?: any;
30
+ error_message?: string;
31
+ }>;
32
+ }
33
+ export interface UseGmVariablesOptions {
34
+ invoker?: GmIpcInvoker;
35
+ /**
36
+ * The HMI's generated tag spec, used when the server cannot describe its
37
+ * own variables. Without it, an old server yields an empty picker.
38
+ */
39
+ fallbackTags?: readonly TagConfig[];
40
+ /** Skip the read entirely (e.g. while a dialog is closed). */
41
+ enabled?: boolean;
42
+ }
43
+ export interface UseGmVariablesResult {
44
+ variables: GmVariable[];
45
+ loading: boolean;
46
+ /**
47
+ * True when the list came from the generated tag spec rather than the
48
+ * server — names and types, no descriptions, and `ux: true` variables only.
49
+ */
50
+ partial: boolean;
51
+ /** Set only when there is nothing to show at all. */
52
+ error: string | null;
53
+ refresh: () => Promise<void>;
54
+ }
55
+ export declare function useGmVariables(options?: UseGmVariablesOptions): UseGmVariablesResult;
56
+ export default useGmVariables;
57
+ //# sourceMappingURL=useGmVariables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGmVariables.d.ts","sourceRoot":"","sources":["../../src/hooks/useGmVariables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,WAAW,YAAY;IACzB,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,qBAAqB;IAClC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACpC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAyBD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,oBAAoB,CAsDpF;AAED,eAAe,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ import{useCallback,useContext,useEffect,useRef,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";function fromTagSpec(e){return e.filter(e=>"string"==typeof e.fqdn&&e.fqdn.toLowerCase().startsWith("gm.")).map(e=>({fqdn:e.fqdn,name:e.fqdn.slice(3),type:String(e.valueType??""),description:"",quantity:e.quantity??e.scale,ux:!0,nonvolatile:!1,writable:!0,is_signal:!1})).sort((e,t)=>e.name.localeCompare(t.name))}export function useGmVariables(e){const t=useContext(EventEmitterContext),a=!1!==e?.enabled,r=e?.invoker??(async(e,a)=>await t.invoke(e,MessageType.Request,a)),s=useRef(r);s.current=r;const n=useRef(e?.fallbackTags);n.current=e?.fallbackTags;const[o,i]=useState([]),[l,u]=useState(a),[c,f]=useState(!1),[m,g]=useState(null),p=useCallback(async()=>{u(!0);try{const e=await s.current("gm.get_variables",{}),t=e?.data?.variables;if(e?.success&&Array.isArray(t))return i(t),f(!1),void g(null);throw new Error(e?.error_message||"gm.get_variables returned no variables")}catch(e){const t=n.current;t&&t.length>0?(i(fromTagSpec(t)),f(!0),g(null)):(i([]),f(!1),g(String(e?.message??e)))}finally{u(!1)}},[]);return useEffect(()=>{a?p():u(!1)},[a,p]),{variables:o,loading:l,partial:c,error:m,refresh:p}}export default useGmVariables;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * useSequenceProducers — the names a method's sequence actually records.
3
+ *
4
+ * A TIS cycle field has two possible producers and the method editor could only
5
+ * ever see one of them:
6
+ *
7
+ * 1. `source: "gm.<var>"` — the generated `add_cycle()` reads the tag.
8
+ * 2. `source: null` — the value arrives with the row, matched by NAME. On
9
+ * gen4_ac that is the sequencer: `store_value` steps fill a row buffer and
10
+ * `tis.add_cycle` writes `cycle_data` verbatim, without checking it against
11
+ * `cycle_fields`.
12
+ *
13
+ * Because nothing checks the second one, both of its failure modes are silent:
14
+ * a declared column nothing stores is blank on every row forever, and a stored
15
+ * name with no column is recorded where nothing displays it. This hook reads
16
+ * the `.seq.json` so the editor can say so.
17
+ *
18
+ * The method id IS the sequence filename and the TIS method id — one name, set
19
+ * by the sequence workbench's New / Save As / Open. A product with no sequencer
20
+ * simply has no such file, which is why "could not read it" resolves to
21
+ * `undefined` and never to `[]`: an empty list would claim every cycle field
22
+ * has no producer.
23
+ */
24
+ export interface UseSequenceProducersOptions {
25
+ /** Override the IPC layer (tests, playground). */
26
+ invoker?: (topic: string, messageType: number, payload: object) => Promise<any>;
27
+ }
28
+ export interface UseSequenceProducersResult {
29
+ /** `store_value` names, or `undefined` when there is no sequence to read. */
30
+ producedNames: string[] | undefined;
31
+ loading: boolean;
32
+ refresh: () => Promise<void>;
33
+ }
34
+ /** Every `store_value` name in a method, `cycle` bodies included. */
35
+ export declare function collectStoredNames(method: any): string[];
36
+ export declare function useSequenceProducers(methodId: string | null, options?: UseSequenceProducersOptions): UseSequenceProducersResult;
37
+ export default useSequenceProducers;
38
+ //# sourceMappingURL=useSequenceProducers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSequenceProducers.d.ts","sourceRoot":"","sources":["../../src/hooks/useSequenceProducers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,MAAM,WAAW,2BAA2B;IACxC,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACnF;AAED,MAAM,WAAW,0BAA0B;IACvC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,EAAE,CAiBxD;AAED,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,OAAO,CAAC,EAAE,2BAA2B,GACtC,0BAA0B,CAkC5B;AAED,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ import{useCallback,useContext,useEffect,useRef,useState}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";const METHOD_DIR="methods",METHOD_EXT=".seq.json";export function collectStoredNames(e){const t=[],o=e=>{if(Array.isArray(e))for(const s of e)s&&"object"==typeof s&&("cycle"===s.type&&o(s.body),"store_value"===s.type&&"string"==typeof s.name&&""!==s.name.trim()&&(t.includes(s.name)||t.push(s.name)))};return o(e?.steps),t}export function useSequenceProducers(e,t){const o=useContext(EventEmitterContext),s=t?.invoker??((e,t,s)=>o.invoke(e,t,s)),r=useRef(s);r.current=s;const[n,a]=useState(void 0),[c,u]=useState(!1),i=useCallback(async()=>{if(e){u(!0);try{const t=`datastore.methods/${e}${METHOD_EXT}`,o=await r.current(t,MessageType.Read,{});if(!1===o?.success)return void a(void 0);const s=o?.data??o;if(!s||!Array.isArray(s.steps))return void a(void 0);a(collectStoredNames(s))}catch{a(void 0)}finally{u(!1)}}else a(void 0)},[e]);return useEffect(()=>{i()},[i]),{producedNames:n,loading:c,refresh:i}}export default useSequenceProducers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.5.7",
3
+ "version": "3.5.11",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  "generate-docs": "typedoc",
13
13
  "build": "npm run clean && tsc && node ./tools/copy-distribution-files.cjs && npm run build-themes && npm run minify",
14
14
  "build:dev": "npm run clean && tsc && node ./tools/copy-distribution-files.cjs",
15
+ "test:tis-editor": "vite build --config tools/tests/vite.config.mjs && node tools/tests/tis-editor.test.mjs",
15
16
  "dev": "vite",
16
17
  "playground": "vite",
17
18
  "playground:build": "vite build",
@@ -21,7 +22,9 @@
21
22
  "release:patch": "npm version patch && npm publish",
22
23
  "release:minor": "npm version minor && npm publish",
23
24
  "release:major": "npm version major && npm publish",
24
- "convert-assets": "npx @svgr/cli --typescript --out-dir src/assets/ src/assets/svg/ --jsx-runtime automatic --ignore-existing"
25
+ "convert-assets": "npx @svgr/cli --typescript --out-dir src/assets/ src/assets/svg/ --jsx-runtime automatic --ignore-existing",
26
+ "test:varpick": "vite build --config tools/tests/vite.config.mjs && node tools/tests/varpick.test.mjs",
27
+ "probe:live": "node tools/tests/live-server.probe.mjs"
25
28
  },
26
29
  "repository": {
27
30
  "type": "git",
@@ -53,6 +56,7 @@
53
56
  }
54
57
  },
55
58
  "dependencies": {
59
+ "@adcops/autocore-varpick": "^1.0.0",
56
60
  "@monaco-editor/react": "^4.7.0",
57
61
  "@tauri-apps/api": "^2.9.1",
58
62
  "chart.js": "^4.5.1",
@@ -82,6 +86,7 @@
82
86
  "@types/react-dom": "^18.2.7",
83
87
  "@types/react-transition-group": "^4.4.12",
84
88
  "@vitejs/plugin-react": "^6.0.2",
89
+ "jsdom": "^22.1.0",
85
90
  "primereact": "^10.9.7",
86
91
  "rimraf": "^6.0.1",
87
92
  "terser": "^5.44.0",
@@ -38,6 +38,9 @@ export type { TestSetupFormProps } from './tis/TestSetupForm';
38
38
  export { TestSetupView } from './tis/TestSetupView';
39
39
  export type { TestSetupViewProps } from './tis/TestSetupView';
40
40
 
41
+ export { orderedViews, renumberViews, moveView } from './tis/chartViews';
42
+ export type { NamedView, OrderableView } from './tis/chartViews';
43
+
41
44
  export { TestFieldRow } from './tis/TestFieldRow';
42
45
  export type { TestFieldRowProps, CycleConfigScope } from './tis/TestFieldRow';
43
46
 
@@ -124,3 +127,20 @@ export { NetworkPanel } from './network/NetworkPanel';
124
127
  export type { NetworkPanelProps } from './network/NetworkPanel';
125
128
  export { StagedChangeBanner } from './network/StagedChangeBanner';
126
129
  export type { StagedChangeBannerProps } from './network/StagedChangeBanner';
130
+
131
+ // -----------------------------------------------------------------------
132
+ // Variable picker — a field with a button that opens a searchable, described
133
+ // list of the machine's variables, so nobody has to type `gm.<something>`
134
+ // from memory. Used by the TIS method editor; exported for any other
135
+ // authoring surface that asks for a machine value.
136
+ // -----------------------------------------------------------------------
137
+ export { VariablePicker, VariableInput } from './varpick';
138
+ export type { VariablePickerProps, VariableInputProps } from './varpick';
139
+ export {
140
+ boundCatalog,
141
+ chartFieldCatalog,
142
+ configSourceCatalog,
143
+ cycleSourceCatalog,
144
+ rawSourceCatalog,
145
+ } from './varpick';
146
+ export type { TisCatalogContext } from './varpick';
@@ -47,6 +47,7 @@ import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
47
47
  import { formatScaled } from '../../core/formatScaled';
48
48
  import type { ScaleConfig } from '../../core/AutoCoreTagTypes';
49
49
  import { useRawCycleData } from './useRawCycleData';
50
+ import { orderedViews } from './chartViews';
50
51
  import { ScienceTable } from './ScienceTable';
51
52
  import type { ScienceColumn } from './ScienceTable';
52
53
 
@@ -171,6 +172,14 @@ export interface ChartView {
171
172
  * table, CSV export and every calculation still see the full value.
172
173
  */
173
174
  x_decimals?: number;
175
+ /**
176
+ * Position in the view picker, ascending — the view with the lowest
177
+ * `order` is the one this panel opens on. Written by the TIS editor's
178
+ * Move up / Move down. See `chartViews.ts`: the keys of `views` arrive
179
+ * sorted by id, so the order an author chose has to be carried
180
+ * explicitly.
181
+ */
182
+ order?: number;
174
183
  /**
175
184
  * Optional shaded X-range bands drawn over the plot, declared per view
176
185
  * in project.json (static). These are drawn on every cycle.
@@ -336,16 +345,13 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
336
345
  const [availableCycles, setAvailableCycles] = useState<number[]>([]);
337
346
  const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
338
347
 
339
- // All views, any type, in declaration order. The dropdown lists
340
- // every one; the chart area dispatches on `view.type` to render
341
- // either a scatter or a raw_trace chart from the appropriate data.
342
- const allViews = useMemo(() => {
343
- const out: { name: string; view: ChartView }[] = [];
344
- for (const [name, v] of Object.entries(schema?.views ?? {})) {
345
- out.push({ name, view: v as ChartView });
346
- }
347
- return out;
348
- }, [schema]);
348
+ // All views, any type, in the author's order (see `orderedViews`). The
349
+ // dropdown lists every one; the chart area dispatches on `view.type` to
350
+ // render either a scatter or a raw_trace chart from the appropriate data.
351
+ const allViews = useMemo(
352
+ () => orderedViews(schema?.views as Record<string, ChartView> | undefined),
353
+ [schema],
354
+ );
349
355
 
350
356
  const [selectedView, setSelectedView] = useState<string | null>(
351
357
  allViews.length > 0 ? allViews[0].name : null,
@@ -24,6 +24,7 @@ import { Line } from 'react-chartjs-2';
24
24
  import type { ChartView, TestMethod } from './TestDataView';
25
25
  import { useTis } from './TisProvider';
26
26
  import { useRawCycleData } from './useRawCycleData';
27
+ import { orderedViews } from './chartViews';
27
28
 
28
29
  ChartJS.register(
29
30
  CategoryScale, LinearScale, PointElement, LineElement,
@@ -55,13 +56,11 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
55
56
  const chartRef = useRef<any>(null);
56
57
 
57
58
  // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
58
- const traceViews = useMemo(() => {
59
- const out: { name: string; view: ChartView }[] = [];
60
- for (const [name, v] of Object.entries(schema?.views ?? {})) {
61
- if ((v as ChartView).type === 'raw_trace') out.push({ name, view: v as ChartView });
62
- }
63
- return out;
64
- }, [schema]);
59
+ // In the author's order, same as the unified panel's picker.
60
+ const traceViews = useMemo(
61
+ () => orderedViews(schema?.views as Record<string, ChartView> | undefined, 'raw_trace'),
62
+ [schema],
63
+ );
65
64
 
66
65
  const [selectedView, setSelectedView] = useState<string | null>(
67
66
  traceViews.length > 0 ? traceViews[0].name : null,
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Chart-view ordering.
3
+ *
4
+ * `views` is a JSON object and the server holds it in a `BTreeMap`, so what
5
+ * reaches the HMI is sorted by view id — never by the order anyone chose. That
6
+ * order is a real setting: the Data tab opens on the FIRST view, so whichever
7
+ * id happens to sort first becomes the default chart. Renaming views to spell
8
+ * an order is not an option (the id is the wire key, and codegen emits it).
9
+ *
10
+ * So the order is carried explicitly, per view, as `order`: an integer,
11
+ * ascending, renumbered densely by the editor's Move up / Move down.
12
+ *
13
+ * `order` is deliberately NOT modelled by the Rust `ChartView`. It rides in
14
+ * that struct's flattened `extra` map, the way `x_decimals`, `smooth` and
15
+ * `regions` already do, so a `tis.put_method` → `tis.save_config` round-trip
16
+ * preserves it with no server-side change to deploy.
17
+ *
18
+ * A view with no `order` sorts after every ordered one, by id — so a method
19
+ * that has never been through the editor keeps exactly the order it shows
20
+ * today, and a newly added view lands last instead of jumping to the front.
21
+ */
22
+
23
+ /** Minimal shape this module needs; both the editor's and the runtime's
24
+ * `ChartView` satisfy it. */
25
+ export interface OrderableView {
26
+ order?: number;
27
+ type?: string;
28
+ }
29
+
30
+ /** A view together with its id — the id is the wire key, not a field. */
31
+ export interface NamedView<V> {
32
+ name: string;
33
+ view: V;
34
+ }
35
+
36
+ /** Sort key for a view that declares no `order`: after all ordered views. */
37
+ const UNORDERED = Number.MAX_SAFE_INTEGER;
38
+
39
+ const orderOf = (v: OrderableView | undefined): number =>
40
+ typeof v?.order === 'number' && Number.isFinite(v.order) ? v.order : UNORDERED;
41
+
42
+ /**
43
+ * The method's views in display order: by `order` ascending, then by id.
44
+ *
45
+ * @param views The method's `views` map, as received from the server.
46
+ * @param ofType Optional `type` filter (e.g. `'raw_trace'`).
47
+ */
48
+ export function orderedViews<V extends OrderableView>(
49
+ views?: Record<string, V> | null,
50
+ ofType?: string,
51
+ ): NamedView<V>[] {
52
+ const out: NamedView<V>[] = [];
53
+ for (const [name, view] of Object.entries(views ?? {})) {
54
+ if (ofType && view?.type !== ofType) continue;
55
+ out.push({ name, view });
56
+ }
57
+ out.sort((a, b) => {
58
+ const d = orderOf(a.view) - orderOf(b.view);
59
+ return d !== 0 ? d : a.name.localeCompare(b.name);
60
+ });
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Stamp `order` 0..n-1 onto `views` following `orderedIds`.
66
+ *
67
+ * Every view is renumbered, not just the moved one: dense integers mean the
68
+ * next move is a swap of two adjacent numbers, and a method that was
69
+ * previously unordered comes out fully ordered after one Move rather than
70
+ * half-and-half.
71
+ *
72
+ * Ids in `views` but missing from `orderedIds` keep their existing `order`
73
+ * and land after the renumbered ones.
74
+ */
75
+ export function renumberViews<V extends OrderableView>(
76
+ views: Record<string, V>,
77
+ orderedIds: string[],
78
+ ): Record<string, V> {
79
+ const out: Record<string, V> = {};
80
+ orderedIds.forEach((id, i) => {
81
+ const v = views[id];
82
+ if (v) out[id] = { ...v, order: i };
83
+ });
84
+ for (const [id, v] of Object.entries(views)) {
85
+ if (!(id in out)) out[id] = v;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * Move the view at `index` by `dir` (-1 up, +1 down) and renumber.
92
+ * Out-of-range moves return the map unchanged.
93
+ */
94
+ export function moveView<V extends OrderableView>(
95
+ views: Record<string, V>,
96
+ index: number,
97
+ dir: -1 | 1,
98
+ ): Record<string, V> {
99
+ const ids = orderedViews(views).map(v => v.name);
100
+ const j = index + dir;
101
+ if (index < 0 || index >= ids.length || j < 0 || j >= ids.length) return views;
102
+ [ids[index], ids[j]] = [ids[j], ids[index]];
103
+ return renumberViews(views, ids);
104
+ }
@@ -105,6 +105,17 @@
105
105
  font-size: 0.8rem;
106
106
  }
107
107
 
108
+ /* A degradation, not a failure: the editor works, it just knows less. Amber
109
+ * rather than the red used for a real error. */
110
+ .tis-editor__notice {
111
+ background: #fffbeb;
112
+ color: #92400e;
113
+ border-left: 3px solid #d97706;
114
+ padding: 0.5rem 1rem;
115
+ margin: 0.5rem 1rem;
116
+ font-size: 0.875rem;
117
+ }
118
+
108
119
  .tis-editor__empty {
109
120
  flex: 1;
110
121
  display: flex;
@@ -31,6 +31,10 @@ import { SaveDiffDialog } from './editor/SaveDiffDialog';
31
31
  import type { TestMethod } from './types';
32
32
 
33
33
  import './TisConfigEditor.css';
34
+ import { useGmVariables } from '../../hooks/useGmVariables';
35
+ import { useSequenceProducers } from '../../hooks/useSequenceProducers';
36
+ import { AuthoringContext } from './editor/AuthoringContext';
37
+ import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
34
38
 
35
39
  export interface TisConfigEditorProps {
36
40
  /** Project ID. Today this is informational (the server hosts one
@@ -70,6 +74,11 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
70
74
  );
71
75
 
72
76
  const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
77
+
78
+ // What the variable pickers offer. Read once for the editor rather than
79
+ // per dialog: the table is ~100 rows and every field dialog wants it.
80
+ const { tags } = useContext(AutoCoreTagContext);
81
+ const gm = useGmVariables({ invoker: effectiveInvoker, fallbackTags: tags });
73
82
  const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
74
83
  const [selectedId, setSelectedId] = useState<string | null>(null);
75
84
  const [draftError, setDraftError] = useState<string | null>(null);
@@ -101,6 +110,15 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
101
110
  // Clear errors whenever the selected method changes.
102
111
  useEffect(() => { setDraftError(null); }, [selectedId]);
103
112
 
113
+ // The sequence that fills this method's cycle columns, if it has one. Keyed
114
+ // on the selected method because the method id IS the sequence filename.
115
+ const { producedNames } = useSequenceProducers(selectedId);
116
+
117
+ const authoring = useMemo(
118
+ () => ({ variables: gm.variables, producedNames, partial: gm.partial }),
119
+ [gm.variables, producedNames, gm.partial],
120
+ );
121
+
104
122
  const onApply = async (next: TestMethod) => {
105
123
  if (!selectedId) return;
106
124
  setBusy(true);
@@ -197,6 +215,7 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
197
215
  };
198
216
 
199
217
  return (
218
+ <AuthoringContext.Provider value={authoring}>
200
219
  <div className="tis-editor">
201
220
  <header className="tis-editor__header">
202
221
  <h2>
@@ -228,6 +247,17 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
228
247
  </div>
229
248
  )}
230
249
 
250
+ {/* Said out loud rather than left implicit: a picker that silently
251
+ lists two thirds of the machine sends an author hunting for a
252
+ variable that is there, just not offered. */}
253
+ {gm.partial && (
254
+ <div className="tis-editor__notice">
255
+ This server cannot describe its own variables, so the pickers are
256
+ listing only the variables published to this HMI, without descriptions.
257
+ Update autocore_server to get the full list.
258
+ </div>
259
+ )}
260
+
231
261
  <div className="tis-editor__body">
232
262
  <aside className="tis-editor__sidebar">
233
263
  <div className="tis-editor__sidebar-actions">
@@ -321,6 +351,7 @@ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, inv
321
351
  onCancel={() => setSaveDialogOpen(false)}
322
352
  />
323
353
  </div>
354
+ </AuthoringContext.Provider>
324
355
  );
325
356
  };
326
357
 
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What the method editor's pickers can offer.
3
+ *
4
+ * A context rather than props because the fields that need it — a field
5
+ * dialog's Source, a bound, a chart axis, a raw column — sit three and four
6
+ * levels below the editor that fetches it, and threading the same two values
7
+ * through every intermediate component is how a prop list becomes noise.
8
+ *
9
+ * Everything here is optional. With none of it the pickers still open; they just
10
+ * have nothing to list, which is exactly the state the editor was in before they
11
+ * existed.
12
+ */
13
+
14
+ import { createContext, useContext } from 'react';
15
+ import type { GmVariable } from '@adcops/autocore-varpick';
16
+
17
+ export interface AuthoringCatalogs {
18
+ /** The machine's GM variable table, from `gm.get_variables`. */
19
+ variables: GmVariable[];
20
+
21
+ /**
22
+ * `store_value` names in the selected method's sequence.
23
+ *
24
+ * `undefined` means "not looked up" — the host could not read the
25
+ * `.seq.json`, or this product has no sequencer. That is deliberately
26
+ * different from `[]`, which means "read it, and it stores nothing": the
27
+ * first must not claim a cycle field has no producer.
28
+ */
29
+ producedNames?: string[];
30
+
31
+ /**
32
+ * True when `variables` came from the generated tag spec rather than the
33
+ * server — names and types, no descriptions, `ux: true` only.
34
+ *
35
+ * Worth surfacing: a picker that silently lists two thirds of the machine
36
+ * is worse than one that says it is listing two thirds of the machine.
37
+ */
38
+ partial?: boolean;
39
+ }
40
+
41
+ export const AuthoringContext = createContext<AuthoringCatalogs>({ variables: [] });
42
+
43
+ export const useAuthoringCatalogs = (): AuthoringCatalogs => useContext(AuthoringContext);
@@ -5,8 +5,11 @@ import { InputText } from 'primereact/inputtext';
5
5
  import { Dropdown } from 'primereact/dropdown';
6
6
  import { InputNumber } from 'primereact/inputnumber';
7
7
  import { FormRow } from '../../forms/FormRow';
8
+ import { useEditDraft } from '../../forms/useEditDraft';
8
9
  import { DEFAULT_X_DECIMALS } from '../../tis/TestDataView';
9
10
  import type { ChartAxis, ChartSeries, ChartView } from '../types';
11
+ import { VariableInput } from '../../varpick';
12
+ import type { Catalog } from '@adcops/autocore-varpick';
10
13
 
11
14
  /** Upper bound on the editor's X-decimals spinner. Past ~6 the tick labels
12
15
  * are wider than the gap between them, which is the problem this setting
@@ -30,6 +33,16 @@ export interface ChartViewDialogProps {
30
33
  onSave: (viewId: string, view: ChartView) => void;
31
34
  /** Known field/column names; offered as datalist suggestions. */
32
35
  knownKeys: string[];
36
+ /**
37
+ * The same names, grouped and described, for the picker button.
38
+ *
39
+ * Takes the view TYPE because it decides which list is the likely answer:
40
+ * a `raw_trace` plots trace columns, a `cycle_scatter` plots cycle fields.
41
+ * A datalist alone was not enough — it only appears once you start typing,
42
+ * which is no help when you do not know what to type, and it does not open
43
+ * at all on a touchscreen.
44
+ */
45
+ fieldCatalog?: (viewType: string) => Catalog;
33
46
  /** Other view ids already in use — for uniqueness validation. */
34
47
  siblingIds: string[];
35
48
  }
@@ -41,19 +54,32 @@ const blank = (): ChartView => ({
41
54
  });
42
55
 
43
56
  export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
44
- visible, initial, onCancel, onSave, knownKeys, siblingIds,
57
+ visible, initial, onCancel, onSave, knownKeys, siblingIds, fieldCatalog,
45
58
  }) => {
46
- const [viewId, setViewId] = useState<string>('');
47
- const [draft, setDraft] = useState<ChartView>(blank());
48
- const [error, setError] = useState<string | null>(null);
59
+ // One draft covering both edited values (the id is editable too), seeded
60
+ // once per open. Keyed on the view id rather than on `initial` — the
61
+ // parent rebuilds that object on every render of its own, and this editor
62
+ // re-renders with the live tag traffic underneath it, so an identity-keyed
63
+ // reseed threw away every keystroke as it was typed.
64
+ const { draft: form, setDraft: setForm, token } = useEditDraft(
65
+ visible,
66
+ initial?.viewId ?? '',
67
+ () => ({
68
+ viewId: initial?.viewId ?? '',
69
+ view: initial
70
+ ? (JSON.parse(JSON.stringify(initial.view)) as ChartView)
71
+ : blank(),
72
+ }),
73
+ );
74
+ const viewId = form.viewId;
75
+ const draft = form.view;
76
+ const setViewId = (id: string) => setForm(f => ({ ...f, viewId: id }));
77
+ const setDraft = (view: ChartView) => setForm(f => ({ ...f, view }));
49
78
 
50
- useEffect(() => {
51
- if (visible) {
52
- setViewId(initial?.viewId ?? '');
53
- setDraft(initial ? JSON.parse(JSON.stringify(initial.view)) : blank());
54
- setError(null);
55
- }
56
- }, [visible, initial]);
79
+ const [error, setError] = useState<string | null>(null);
80
+ // Clears on open and on a switch to another view — `token` changes on
81
+ // exactly those transitions and on no other render.
82
+ useEffect(() => { setError(null); }, [token]);
57
83
 
58
84
  const validate = (): string | null => {
59
85
  if (!viewId.trim()) return 'View ID is required.';
@@ -110,10 +136,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
110
136
  />
111
137
  </FormRow>
112
138
  <FormRow label={isRawTrace ? 'X column' : 'X field'} required>
113
- <InputText
139
+ <VariableInput
114
140
  value={(draft.x[axisKey] as string) ?? ''}
115
- onChange={(e) => setAxis({ [axisKey]: e.target.value } as ChartAxis)}
116
- list="known-keys"
141
+ onChange={(v) => setAxis({ [axisKey]: v } as ChartAxis)}
142
+ catalog={() => fieldCatalog?.(draft.type) ?? []}
143
+ ariaLabel="Choose the X axis column"
144
+ pickerTitle="X axis"
145
+ pickerHint="A chart naming a column that does not exist draws an empty plot with no error, which reads as “the machine recorded nothing”."
117
146
  />
118
147
  </FormRow>
119
148
  <FormRow label="X label">
@@ -159,11 +188,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
159
188
  alignItems: 'center',
160
189
  }}
161
190
  >
162
- <InputText
191
+ <VariableInput
163
192
  placeholder={isRawTrace ? 'column' : 'field'}
164
193
  value={(s[axisKey] as string) ?? ''}
165
- onChange={(e) => setSeries(i, { [axisKey]: e.target.value } as ChartSeries)}
166
- list="known-keys"
194
+ onChange={(v) => setSeries(i, { [axisKey]: v } as ChartSeries)}
195
+ catalog={() => fieldCatalog?.(draft.type) ?? []}
196
+ ariaLabel={`Choose the column for series ${i + 1}`}
197
+ pickerTitle="Y series"
167
198
  />
168
199
  <InputText
169
200
  placeholder="label"
@@ -148,6 +148,7 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
148
148
  siblingNames={fields.map(f => f.name)}
149
149
  isResultsField={arrayKey === 'results_fields'}
150
150
  isCycleConfigField={arrayKey === 'cycle_config_fields'}
151
+ isCycleField={arrayKey === 'cycle_fields'}
151
152
  cycleFieldNames={(method.cycle_fields ?? []).map(f => f.name).filter(Boolean)}
152
153
  resultsFieldNames={(method.results_fields ?? []).map(f => f.name).filter(Boolean)}
153
154
  onCancel={() => setDialogOpen(false)}