@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.
- package/dist/components/forms/useEditDraft.d.ts +49 -0
- package/dist/components/forms/useEditDraft.d.ts.map +1 -0
- package/dist/components/forms/useEditDraft.js +1 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +8 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
- package/dist/components/tis/TestRawDataView.js +1 -1
- package/dist/components/tis/chartViews.d.ts +57 -0
- package/dist/components/tis/chartViews.d.ts.map +1 -0
- package/dist/components/tis/chartViews.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +11 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
- package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +6 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/varpick/VariableInput.d.ts +41 -0
- package/dist/components/varpick/VariableInput.d.ts.map +1 -0
- package/dist/components/varpick/VariableInput.js +1 -0
- package/dist/components/varpick/VariablePicker.d.ts +45 -0
- package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
- package/dist/components/varpick/VariablePicker.js +1 -0
- package/dist/components/varpick/index.d.ts +16 -0
- package/dist/components/varpick/index.d.ts.map +1 -0
- package/dist/components/varpick/index.js +1 -0
- package/dist/components/varpick/tisCatalogs.d.ts +85 -0
- package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
- package/dist/components/varpick/tisCatalogs.js +1 -0
- package/dist/components/varpick/varpick.css +167 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/core/AutoCoreTagTypes.d.ts +13 -0
- package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useGmVariables.d.ts +57 -0
- package/dist/hooks/useGmVariables.d.ts.map +1 -0
- package/dist/hooks/useGmVariables.js +1 -0
- package/dist/hooks/useSequenceProducers.d.ts +38 -0
- package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
- package/dist/hooks/useSequenceProducers.js +1 -0
- package/package.json +7 -2
- package/src/components/forms/useEditDraft.ts +0 -0
- package/src/components/index.ts +20 -0
- package/src/components/tis/TestDataView.tsx +16 -10
- package/src/components/tis/TestRawDataView.tsx +6 -7
- package/src/components/tis/chartViews.ts +104 -0
- package/src/components/tis-editor/TisConfigEditor.css +11 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
- package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
- package/src/components/tis-editor/editor/ChartViewDialog.tsx +48 -17
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
- package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +90 -23
- package/src/components/tis-editor/editor/ViewsEditor.tsx +56 -5
- package/src/components/tis-editor/types.ts +6 -0
- package/src/components/varpick/VariableInput.tsx +126 -0
- package/src/components/varpick/VariablePicker.tsx +168 -0
- package/src/components/varpick/index.ts +22 -0
- package/src/components/varpick/tisCatalogs.ts +235 -0
- package/src/components/varpick/varpick.css +167 -0
- package/src/core/AutoCoreTagContext.tsx +3 -1
- package/src/core/AutoCoreTagTypes.ts +14 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useGmVariables.ts +142 -0
- package/src/hooks/useSequenceProducers.ts +104 -0
- package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
- package/tools/tests/dist/autocore-react.css +257 -0
- package/tools/tests/dist/tis-editor.entry.js +9295 -0
- package/tools/tests/dist/varpick.entry.js +113 -0
- package/tools/tests/live-server.probe.mjs +172 -0
- package/tools/tests/tis-editor.entry.tsx +46 -0
- package/tools/tests/tis-editor.test.mjs +133 -0
- package/tools/tests/varpick.entry.tsx +81 -0
- package/tools/tests/varpick.test.mjs +208 -0
- package/tools/tests/vite.config.mjs +35 -0
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -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.
|
|
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",
|
|
Binary file
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
|
340
|
-
// every one; the chart area dispatches on `view.type` to
|
|
341
|
-
// either a scatter or a raw_trace chart from the appropriate data.
|
|
342
|
-
const allViews = useMemo(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
<
|
|
139
|
+
<VariableInput
|
|
114
140
|
value={(draft.x[axisKey] as string) ?? ''}
|
|
115
|
-
onChange={(
|
|
116
|
-
|
|
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
|
-
<
|
|
191
|
+
<VariableInput
|
|
163
192
|
placeholder={isRawTrace ? 'column' : 'field'}
|
|
164
193
|
value={(s[axisKey] as string) ?? ''}
|
|
165
|
-
onChange={(
|
|
166
|
-
|
|
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)}
|