@adcops/autocore-react 3.3.57 → 3.3.59
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/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ProjectInfoDialog.d.ts +21 -0
- package/dist/components/tis/ProjectInfoDialog.d.ts.map +1 -0
- package/dist/components/tis/ProjectInfoDialog.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +4 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestMethodDialog.d.ts +17 -0
- package/dist/components/tis/TestMethodDialog.d.ts.map +1 -0
- package/dist/components/tis/TestMethodDialog.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +13 -8
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +6 -0
- package/src/components/tis/ProjectInfoDialog.tsx +307 -0
- package/src/components/tis/TestDataView.tsx +4 -0
- package/src/components/tis/TestMethodDialog.tsx +147 -0
- package/src/components/tis/TestSetupForm.tsx +323 -85
|
@@ -2,6 +2,10 @@ export { TisProvider, TisContext, useTis, useTisSchemas, useTisState, useTisSele
|
|
|
2
2
|
export type { TisProviderProps, TisContextValue, TisLiveState, TisSelection, TisSelectionPatch, TisRunCacheEntry, SchemaRegistry, TisMethodSchema, } from './tis/TisProvider';
|
|
3
3
|
export { TestSetupForm } from './tis/TestSetupForm';
|
|
4
4
|
export type { TestSetupFormProps } from './tis/TestSetupForm';
|
|
5
|
+
export { ProjectInfoDialog } from './tis/ProjectInfoDialog';
|
|
6
|
+
export type { ProjectInfoDialogProps, ProjectInfoMode } from './tis/ProjectInfoDialog';
|
|
7
|
+
export { TestMethodDialog } from './tis/TestMethodDialog';
|
|
8
|
+
export type { TestMethodDialogProps } from './tis/TestMethodDialog';
|
|
5
9
|
export { ResultHistoryTable } from './tis/ResultHistoryTable';
|
|
6
10
|
export type { ResultHistoryTableProps } from './tis/ResultHistoryTable';
|
|
7
11
|
export { TestDataView } from './tis/TestDataView';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACH,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,WAAW,EACX,eAAe,EACf,UAAU,EACV,SAAS,GACZ,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACR,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,aAAa,EAAE,MAAY,qBAAqB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAO,0BAA0B,CAAC;AAC/D,YAAY,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,MAAa,oBAAoB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7G,OAAO,EAAE,eAAe,EAAE,MAAU,uBAAuB,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACH,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,WAAW,EACX,eAAe,EACf,UAAU,EACV,SAAS,GACZ,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACR,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,aAAa,EAAE,MAAY,qBAAqB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAQ,yBAAyB,CAAC;AAC9D,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAEvF,OAAO,EAAE,gBAAgB,EAAE,MAAS,wBAAwB,CAAC;AAC7D,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAO,0BAA0B,CAAC;AAC/D,YAAY,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,MAAa,oBAAoB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7G,OAAO,EAAE,eAAe,EAAE,MAAU,uBAAuB,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{TisProvider,TisContext,useTis,useTisSchemas,useTisState,useTisSelection,useTisRuns,useTisRun}from"./tis/TisProvider";export{TestSetupForm}from"./tis/TestSetupForm";export{ResultHistoryTable}from"./tis/ResultHistoryTable";export{TestDataView}from"./tis/TestDataView";export{TestRawDataView}from"./tis/TestRawDataView";
|
|
1
|
+
export{TisProvider,TisContext,useTis,useTisSchemas,useTisState,useTisSelection,useTisRuns,useTisRun}from"./tis/TisProvider";export{TestSetupForm}from"./tis/TestSetupForm";export{ProjectInfoDialog}from"./tis/ProjectInfoDialog";export{TestMethodDialog}from"./tis/TestMethodDialog";export{ResultHistoryTable}from"./tis/ResultHistoryTable";export{TestDataView}from"./tis/TestDataView";export{TestRawDataView}from"./tis/TestRawDataView";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TestFieldDef } from './TestSetupForm';
|
|
3
|
+
export type ProjectInfoMode = 'create' | 'edit';
|
|
4
|
+
export interface ProjectInfoDialogProps {
|
|
5
|
+
visible: boolean;
|
|
6
|
+
onHide: () => void;
|
|
7
|
+
mode: ProjectInfoMode;
|
|
8
|
+
/** Project ID being created (`create` mode) or edited (`edit`). */
|
|
9
|
+
projectId: string;
|
|
10
|
+
/** Schema field defs for `project_fields` (from the selected method). */
|
|
11
|
+
projectFields: TestFieldDef[];
|
|
12
|
+
/**
|
|
13
|
+
* Called after a successful create/update with the values that
|
|
14
|
+
* landed on the server. Used by the parent form to refresh its
|
|
15
|
+
* known-projects list and to fold the new fields into subsequent
|
|
16
|
+
* `tis.stage_test` payloads without an extra round-trip.
|
|
17
|
+
*/
|
|
18
|
+
onSubmitted: (projectId: string, projectFields: Record<string, any>) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const ProjectInfoDialog: React.FC<ProjectInfoDialogProps>;
|
|
21
|
+
//# sourceMappingURL=ProjectInfoDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectInfoDialog.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ProjectInfoDialog.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAShF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC;IACtB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC;CAChF;AAUD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA+O9D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Dialog}from"primereact/dialog";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0;export const ProjectInfoDialog=({visible:e,onHide:t,mode:r,projectId:o,projectFields:n,onSubmitted:s})=>{const{invoke:i,write:a}=useContext(EventEmitterContext),{rawValues:c,findTagByFqdn:l}=useContext(AutoCoreTagContext),[m,p]=useState({}),[u,d]=useState(!1),[f,j]=useState(null),x=useRef("");useEffect(()=>{if(!e)return;const t=`${r}:${o}`;if(x.current===t)return;x.current=t;let s=!1;j(null);const a={};for(const e of n){if(!e.source)continue;const t=l(e.source);if(!t)continue;const r=c[t.tagName];null!=r&&(a[e.name]=r)}return"create"===r?s||p(a):(async()=>{try{const e=await i("tis.read_project",MessageType.Request,{project_id:o});if(s)return;if(e?.success){const t={...e.data?.project_fields??{}};for(const e of n)e.source&&void 0!==a[e.name]&&(t[e.name]=a[e.name]);p(t)}else j(e?.error_message??"Failed to read project")}catch(e){s||j(String(e instanceof Error?e.message:e))}})(),()=>{s=!0}},[e,o,r]),useEffect(()=>{e||(x.current="")},[e]);const g=(e,t)=>{p(r=>({...r,[e.name]:t}))},h=useMemo(()=>{for(const e of n){if(!e.required)continue;const t=m[e.name];if(null==t||""===t)return!1}return!0},[n,m]),_=_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem"},children:[_jsx(Button,{label:"Cancel",icon:"pi pi-times",onClick:t,disabled:u,text:!0}),_jsx(Button,{label:"create"===r?"Create Project":"Save",icon:u?"pi pi-spin pi-spinner":"create"===r?"pi pi-plus":"pi pi-check",onClick:async()=>{if(h&&!u){d(!0);try{for(const e of n){if(!e.source)continue;const t=m[e.name];if(null!=t)try{await a(e.source,t)}catch(e){}}const e={};for(const t of n)void 0!==m[t.name]&&(e[t.name]=m[t.name]);const c="create"===r?"tis.create_project":"tis.update_project",l=await i(c,MessageType.Request,{project_id:o,project_fields:e});l?.success?(s(o,e),t()):alert(`Failed: ${l?.error_message??"unknown error"}`)}catch(e){alert(`Failed: ${e instanceof Error?e.message:String(e)}`)}finally{d(!1)}}},disabled:!h||u})]});return _jsxs(Dialog,{header:"create"===r?`Create project: ${o||"(no ID)"}`:`Edit project information: ${o}`,visible:e,onHide:t,footer:_,modal:!0,style:{width:"min(640px, 90vw)"},closable:!u,children:[f&&_jsx("div",{style:{color:"var(--red-500)",marginBottom:"1rem"},children:f}),0===n.length?_jsx("p",{style:{color:"var(--text-secondary-color)"},children:"create"===r?`Click "Create Project" to create the empty project "${o}". This method declares no project_fields, so there's nothing to fill in.`:"This method declares no project_fields, so there's nothing to edit."}):_jsx("div",{className:"ac-form-grid",style:{padding:"0.25rem 0",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:n.map(e=>{const t=(e=>{if(!e.required)return!0;const t=m[e.name];return null!=t&&""!==t})(e),r="string"!==e.type&&"bool"!==e.type,o=`acProjInfo_${e.name}`;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),r?_jsx(ValueInput,{label:void 0,value:null!=m[e.name]?Number(m[e.name]):null,onValueChanged:t=>g(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=m[e.name]?String(m[e.name]):"",onValueChanged:t=>g(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsxs(_Fragment,{children:[_jsx(Tooltip,{target:`#${o}`,position:"left"}),_jsx("span",{id:o,"data-pr-tooltip":e.description,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",cursor:"help"},children:_jsx("i",{className:"pi pi-info-circle",style:{color:"var(--text-secondary-color)"}})})]}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)})})]})};
|
|
@@ -39,6 +39,10 @@ export interface TestMethod {
|
|
|
39
39
|
views?: {
|
|
40
40
|
[name: string]: ChartView;
|
|
41
41
|
};
|
|
42
|
+
/** Optional pretty label for the Test Method picker. */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Optional long-form description for the picker. */
|
|
45
|
+
description?: string;
|
|
42
46
|
}
|
|
43
47
|
export interface TestDataViewProps {
|
|
44
48
|
/** Optional override; defaults to `useTisSelection().projectId`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AAUA,OAAO,KAA2D,MAAM,OAAO,CAAC;AA6BhF,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IAAI,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AACD,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC;AACD,MAAM,WAAW,UAAU;IACvB,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,aAAa,EAAI,YAAY,EAAE,CAAC;IAChC,YAAY,EAAK,YAAY,EAAE,CAAC;IAChC,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAQ,YAAY,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAW;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AAUA,OAAO,KAA2D,MAAM,OAAO,CAAC;AA6BhF,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IAAI,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AACD,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC;AACD,MAAM,WAAW,UAAU;IACvB,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,aAAa,EAAI,YAAY,EAAE,CAAC;IAChC,YAAY,EAAK,YAAY,EAAE,CAAC;IAChC,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAQ,YAAY,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAW;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC/C,wDAAwD;IACxD,KAAK,CAAC,EAAW,MAAM,CAAC;IACxB,qDAAqD;IACrD,WAAW,CAAC,EAAK,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAC9B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,8EAA8E;IAC9E,UAAU,CAAC,EAAG,MAAM,CAAC;IACrB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgPpD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface TestMethodDialogProps {
|
|
3
|
+
visible: boolean;
|
|
4
|
+
onHide: () => void;
|
|
5
|
+
/** Method ID currently selected on the form. The dropdown opens
|
|
6
|
+
* pointing at this value so the dialog reflects current state. */
|
|
7
|
+
currentMethodId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Called with the chosen method_id when the operator clicks OK.
|
|
10
|
+
* Cancel does not fire this callback. The parent is responsible
|
|
11
|
+
* for actually applying the new selection (e.g., updating the
|
|
12
|
+
* provider's selection or local state).
|
|
13
|
+
*/
|
|
14
|
+
onSelected: (methodId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const TestMethodDialog: React.FC<TestMethodDialogProps>;
|
|
17
|
+
//# sourceMappingURL=TestMethodDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestMethodDialog.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestMethodDialog.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAO5D,MAAM,WAAW,qBAAqB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;uEACmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAOD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAsG5D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useEffect,useMemo,useState}from"react";import{Button}from"primereact/button";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{useTisSchemas}from"./TisProvider";const methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e;export const TestMethodDialog=({visible:e,onHide:t,currentMethodId:o,onSelected:r})=>{const s=useTisSchemas(),[l,i]=useState(o);useEffect(()=>{e&&i(o)},[e,o]);const d=useMemo(()=>Object.keys(s).map(e=>({label:methodLabelOf(e,s[e]),value:e})),[s]),a=s[l],n=a?.description&&a.description.length>0?a.description:null,c=_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem"},children:[_jsx(Button,{label:"Cancel",icon:"pi pi-times",onClick:t,text:!0}),_jsx(Button,{label:"OK",icon:"pi pi-check",onClick:()=>{l&&l!==o&&r(l),t()},disabled:!l})]});return _jsx(Dialog,{header:"Select Test Method",visible:e,onHide:t,footer:c,modal:!0,style:{width:"min(560px, 90vw)"},children:0===d.length?_jsxs("p",{style:{color:"var(--text-secondary-color)"},children:["No test methods are declared in this project's ",_jsx("code",{children:"test_methods"})," block."]}):_jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[_jsx("label",{htmlFor:"acTestMethodDropdown",style:{flexShrink:0},children:"Test Method:"}),_jsx(Dropdown,{inputId:"acTestMethodDropdown",value:l,options:d,onChange:e=>i(e.value),placeholder:"Select a method",style:{flex:1}})]}),_jsx("div",{style:{padding:"0.75rem 1rem",background:"var(--surface-100)",borderRadius:"6px",minHeight:"4.5rem",color:n?"var(--text-color)":"var(--text-secondary-color)",fontStyle:n?"normal":"italic",whiteSpace:"pre-wrap"},children:n??"No description provided for this test method."})]})})};
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export interface TestFieldDef {
|
|
3
|
+
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
3
4
|
name: string;
|
|
4
5
|
type: string;
|
|
5
6
|
units?: string;
|
|
6
7
|
required?: boolean;
|
|
7
8
|
source?: string;
|
|
9
|
+
/** Pretty label rendered by the form. Falls back to `name`. Units
|
|
10
|
+
* are appended automatically; don't pre-format `[mm]` into label. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
13
|
+
description?: string;
|
|
8
14
|
}
|
|
9
15
|
export interface TestMethod {
|
|
10
16
|
project_fields: TestFieldDef[];
|
|
11
17
|
config_fields: TestFieldDef[];
|
|
12
18
|
cycle_fields: TestFieldDef[];
|
|
13
19
|
results_fields: TestFieldDef[];
|
|
20
|
+
/** Optional pretty label for the Test Method picker. Falls back
|
|
21
|
+
* to the canonical method_id key. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Optional long-form description shown in the picker dialog
|
|
24
|
+
* when this method is highlighted. */
|
|
25
|
+
description?: string;
|
|
14
26
|
}
|
|
15
27
|
/**
|
|
16
28
|
* Props are all optional overrides — by default the form drives itself
|
|
17
|
-
* from the surrounding `<TisProvider>`.
|
|
18
|
-
* particular axis.
|
|
19
|
-
*
|
|
20
|
-
* - `schema`: bypass `useTisSchemas()` for the selected method.
|
|
21
|
-
* - `defaultProjectId` / `defaultMethodId`: seed the form's local
|
|
22
|
-
* state when the corresponding TIS-context fields are blank.
|
|
23
|
-
* - `onProjectChange` / `onMethodChange` / `onValidationChange`:
|
|
24
|
-
* receive callbacks in addition to the provider's selection state.
|
|
29
|
+
* from the surrounding `<TisProvider>`.
|
|
25
30
|
*/
|
|
26
31
|
export interface TestSetupFormProps {
|
|
27
32
|
schema?: TestMethod;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAehF,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AA4BD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAsdtD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,
|
|
1
|
+
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo,useRef}from"react";import{AutoComplete}from"primereact/autocomplete";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{ProjectInfoDialog}from"./ProjectInfoDialog";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,PROJECT_ID_RE=/^[A-Za-z0-9_-]+$/,isValidProjectIdFormat=e=>PROJECT_ID_RE.test(e);export const TestSetupForm=({schema:e,defaultProjectId:t,defaultMethodId:s,onProjectChange:i,onMethodChange:o,onValidationChange:a})=>{const r=useTis(),{invoke:n,write:l}=useContext(EventEmitterContext),{rawValues:c,findTagByFqdn:d}=useContext(AutoCoreTagContext),p=useMemo(()=>Object.keys(r.schemas),[r.schemas]),[m,u]=useState(r.selection.projectId||t||""),[f,h]=useState(r.selection.methodId||s||r.defaultMethodId||""),[x,j]=useState(""),[g,_]=useState({}),y=e??(f?r.schemas[f]:void 0);useEffect(()=>{r.selection.projectId!==m&&r.setSelection({projectId:m}),i&&i(m)},[m]),useEffect(()=>{r.selection.methodId!==f&&f&&r.setSelection({methodId:f}),o&&o(f)},[f]),useEffect(()=>{r.selection.sampleId!==x&&r.setSelection({sampleId:x})},[x]),useEffect(()=>{r.state.stagedSampleId&&r.state.stagedSampleId!==x&&j(r.state.stagedSampleId)},[r.state.stagedSampleId]);const[I,v]=useState([]),[b,C]=useState([]),S=useRef(new Set),[T,N]=useState(0),[E,M]=useState(!1),[D,P]=useState({}),k=D[m]??{},[w,F]=useState(!1),[O,R]=useState(!1),[V,q]=useState(!1),A=async()=>{try{const e=await n("tis.list_projects",MessageType.Request,{});e.success&&e.data&&e.data.projects&&v(e.data.projects)}catch(e){}};useEffect(()=>{A()},[n]);const L=useMemo(()=>{const e=new Set(I);for(const t of S.current)e.add(t);return e},[I,T]),$=""!==m.trim()&&L.has(m.trim()),B=isValidProjectIdFormat(m.trim()),H=""!==m.trim()&&B&&!L.has(m.trim());useEffect(()=>{const e=m.trim();if(!e||!$)return;if(void 0!==D[e])return;let t=!1;return(async()=>{try{const s=await n("tis.read_project",MessageType.Request,{project_id:e});if(t)return;if(s?.success){const t=s.data?.project_fields??{};P(s=>({...s,[e]:t}))}}catch(e){}})(),()=>{t=!0}},[m,$]),useEffect(()=>{y&&_(e=>{let t=e;for(const s of y.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const i=d(s.source);if(!i)continue;const o=c[i.tagName];null!=o&&(t[s.name]!==o&&(t===e&&(t={...e}),t[s.name]=o))}return t})},[y,c,d]);useEffect(()=>{if(!y)return void M(!1);let e=!0;$||(e=!1),f.trim()||(e=!1),x.trim()||(e=!1);for(const t of y.config_fields)if("sample_id"!==t.name&&t.required){const s=g[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(e&&$&&void 0===D[m.trim()]&&(e=!1),M(e),a&&a(e,g),e){const{sample_id:e,...t}=g??{},s={...k,...t};n("tis.stage_test",MessageType.Request,{project_id:m,method_id:f,sample_id:x,config:s}).catch(e=>{})}},[g,y,m,f,x,$,D,a,n]);const z=async(e,t)=>{if(_({...g,[e.name]:t}),e.source)try{await l(e.source,t)}catch(e){}},J=(e,t)=>{S.current.add(e),N(e=>e+1),P(s=>({...s,[e]:t})),A(),m.trim()!==e&&u(e)};if(!y)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:r.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});const Z=$;return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Project & Method",_jsx("span",{style:{color:E?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:E?"pi pi-check-circle":"pi pi-exclamation-circle"})})]}),_jsx("span",{className:"ac-form-label",children:"Project ID"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(AutoComplete,{value:m,suggestions:b,completeMethod:e=>{const t=e.query.toLowerCase();C(I.filter(e=>e.toLowerCase().includes(t)))},onChange:e=>(e=>{const t=(e||"").replace(/[^a-zA-Z0-9_-]/g,"");u(t)})(e.value),dropdown:!0,placeholder:"Select an existing Project ID, or type a new one and click +",className:Z?"":"p-invalid",style:{flex:1}}),_jsx(Button,{icon:"pi pi-plus",type:"button",onClick:()=>F(!0),disabled:!H,tooltip:m.trim()?B?L.has(m.trim())?"Project already exists":`Create project "${m.trim()}"`:"Letters, digits, _ and - only":"Type a project ID first",tooltipOptions:{position:"top"}}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>R(!0),disabled:!$,tooltip:$?`Edit information for "${m.trim()}"`:"Select an existing project to edit",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:Z?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:Z?"pi pi-check":"pi pi-times"})}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:x,onValueChanged:e=>{j(e)},className:x.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:x.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:x.trim()?"pi pi-check":"pi pi-times"})}),p.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(f,y),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>q(!0),tooltip:p.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:f?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:f?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),y.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=g[e.name];return void 0!==t&&""!==t&&null!==t})(e),s="string"!==e.type&&"bool"!==e.type,i=`acFormInfo_${e.name}`;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(g[e.name]):null,onValueChanged:t=>z(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>z(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsxs(_Fragment,{children:[_jsx(Tooltip,{target:`#${i}`,position:"left"}),_jsx("span",{id:i,"data-pr-tooltip":e.description,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",cursor:"help"},children:_jsx("i",{className:"pi pi-info-circle",style:{color:"var(--text-secondary-color)"}})})]}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(ProjectInfoDialog,{visible:w,onHide:()=>F(!1),mode:"create",projectId:m.trim(),projectFields:y.project_fields,onSubmitted:J}),_jsx(ProjectInfoDialog,{visible:O,onHide:()=>R(!1),mode:"edit",projectId:m.trim(),projectFields:y.project_fields,onSubmitted:J}),_jsx(TestMethodDialog,{visible:V,onHide:()=>q(!1),currentMethodId:f,onSelected:e=>h(e)})]})};
|
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -30,6 +30,12 @@ export type {
|
|
|
30
30
|
export { TestSetupForm } from './tis/TestSetupForm';
|
|
31
31
|
export type { TestSetupFormProps } from './tis/TestSetupForm';
|
|
32
32
|
|
|
33
|
+
export { ProjectInfoDialog } from './tis/ProjectInfoDialog';
|
|
34
|
+
export type { ProjectInfoDialogProps, ProjectInfoMode } from './tis/ProjectInfoDialog';
|
|
35
|
+
|
|
36
|
+
export { TestMethodDialog } from './tis/TestMethodDialog';
|
|
37
|
+
export type { TestMethodDialogProps } from './tis/TestMethodDialog';
|
|
38
|
+
|
|
33
39
|
export { ResultHistoryTable } from './tis/ResultHistoryTable';
|
|
34
40
|
export type { ResultHistoryTableProps } from './tis/ResultHistoryTable';
|
|
35
41
|
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <ProjectInfoDialog> — operator-facing setup for a project's
|
|
5
|
+
* `project_fields`. Used in two modes:
|
|
6
|
+
*
|
|
7
|
+
* - `create`: opened by the `+` button on <TestSetupForm>. The
|
|
8
|
+
* project doesn't exist yet; on submit we call `tis.create_project`
|
|
9
|
+
* with the user-entered fields baked into the request payload, so
|
|
10
|
+
* the directory and project.json materialise atomically. The form
|
|
11
|
+
* pre-fills source-bound fields from live GM values via the
|
|
12
|
+
* surrounding AutoCoreTagProvider so the operator isn't typing
|
|
13
|
+
* things the rig already knows.
|
|
14
|
+
*
|
|
15
|
+
* - `edit`: opened by the pencil button on <TestSetupForm>. The
|
|
16
|
+
* project already exists; we fetch project.json via
|
|
17
|
+
* `tis.read_project` and pre-fill from it, with source-bound
|
|
18
|
+
* fields preferring the live GM value over the persisted one (so
|
|
19
|
+
* "edit" reflects current reality). On submit we
|
|
20
|
+
* `tis.update_project` and `write()` source-bound fields back to
|
|
21
|
+
* GM in the same pass.
|
|
22
|
+
*
|
|
23
|
+
* The dialog never stages or starts a test — it only manages
|
|
24
|
+
* project-level metadata. The future per-user permission gate that
|
|
25
|
+
* locks "create/edit project" away from the operator role hangs off
|
|
26
|
+
* the visibility of the two buttons in <TestSetupForm>; this dialog
|
|
27
|
+
* is agnostic to who opened it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
31
|
+
import { Button } from 'primereact/button';
|
|
32
|
+
import { Dialog } from 'primereact/dialog';
|
|
33
|
+
import { Tooltip } from 'primereact/tooltip';
|
|
34
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
35
|
+
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
36
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
37
|
+
import { ValueInput } from '../ValueInput';
|
|
38
|
+
import { TextInput } from '../TextInput';
|
|
39
|
+
import type { TestFieldDef } from './TestSetupForm';
|
|
40
|
+
|
|
41
|
+
export type ProjectInfoMode = 'create' | 'edit';
|
|
42
|
+
|
|
43
|
+
export interface ProjectInfoDialogProps {
|
|
44
|
+
visible: boolean;
|
|
45
|
+
onHide: () => void;
|
|
46
|
+
mode: ProjectInfoMode;
|
|
47
|
+
/** Project ID being created (`create` mode) or edited (`edit`). */
|
|
48
|
+
projectId: string;
|
|
49
|
+
/** Schema field defs for `project_fields` (from the selected method). */
|
|
50
|
+
projectFields: TestFieldDef[];
|
|
51
|
+
/**
|
|
52
|
+
* Called after a successful create/update with the values that
|
|
53
|
+
* landed on the server. Used by the parent form to refresh its
|
|
54
|
+
* known-projects list and to fold the new fields into subsequent
|
|
55
|
+
* `tis.stage_test` payloads without an extra round-trip.
|
|
56
|
+
*/
|
|
57
|
+
onSubmitted: (projectId: string, projectFields: Record<string, any>) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const labelOf = (f: TestFieldDef): string => {
|
|
61
|
+
const base = f.label && f.label.length > 0 ? f.label : f.name;
|
|
62
|
+
return f.units ? `${base} [${f.units}]` : base;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const hasDescription = (f: TestFieldDef): boolean =>
|
|
66
|
+
typeof f.description === 'string' && f.description.length > 0;
|
|
67
|
+
|
|
68
|
+
export const ProjectInfoDialog: React.FC<ProjectInfoDialogProps> = ({
|
|
69
|
+
visible, onHide, mode, projectId, projectFields, onSubmitted,
|
|
70
|
+
}) => {
|
|
71
|
+
const { invoke, write } = useContext(EventEmitterContext);
|
|
72
|
+
const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
|
|
73
|
+
|
|
74
|
+
const [values, setValues] = useState<Record<string, any>>({});
|
|
75
|
+
const [submitting, setSubmitting] = useState(false);
|
|
76
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
77
|
+
const lastLoadedIdRef = useRef<string>('');
|
|
78
|
+
|
|
79
|
+
// Whenever the dialog opens (or the target project changes while
|
|
80
|
+
// open), reset state and re-load. We DON'T do this in a single
|
|
81
|
+
// useEffect on `[visible, projectId, mode]` because the load is
|
|
82
|
+
// async and we want to drop stale results.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!visible) return;
|
|
85
|
+
const loadKey = `${mode}:${projectId}`;
|
|
86
|
+
if (lastLoadedIdRef.current === loadKey) return;
|
|
87
|
+
lastLoadedIdRef.current = loadKey;
|
|
88
|
+
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
setLoadError(null);
|
|
91
|
+
|
|
92
|
+
// Build the initial value map. For source-bound fields, seed
|
|
93
|
+
// from live GM via the AutoCoreTagProvider's rawValues. For
|
|
94
|
+
// non-source fields the seed is empty in `create` mode and
|
|
95
|
+
// gets overwritten by the persisted value in `edit` mode.
|
|
96
|
+
const seed: Record<string, any> = {};
|
|
97
|
+
for (const f of projectFields) {
|
|
98
|
+
if (!f.source) continue;
|
|
99
|
+
const tag = findTagByFqdn(f.source);
|
|
100
|
+
if (!tag) continue;
|
|
101
|
+
const v = rawValues[tag.tagName];
|
|
102
|
+
if (v !== undefined && v !== null) seed[f.name] = v;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (mode === 'create') {
|
|
106
|
+
if (!cancelled) setValues(seed);
|
|
107
|
+
} else {
|
|
108
|
+
(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const resp: any = await invoke(
|
|
111
|
+
'tis.read_project' as any, MessageType.Request,
|
|
112
|
+
{ project_id: projectId } as any,
|
|
113
|
+
);
|
|
114
|
+
if (cancelled) return;
|
|
115
|
+
if (resp?.success) {
|
|
116
|
+
const persisted = (resp.data?.project_fields ?? {}) as Record<string, any>;
|
|
117
|
+
// Seed (source-bound from GM) + persisted +
|
|
118
|
+
// GM wins on conflicts for source fields,
|
|
119
|
+
// because GM is the live source of truth.
|
|
120
|
+
const merged: Record<string, any> = { ...persisted };
|
|
121
|
+
for (const f of projectFields) {
|
|
122
|
+
if (f.source && seed[f.name] !== undefined) {
|
|
123
|
+
merged[f.name] = seed[f.name];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
setValues(merged);
|
|
127
|
+
} else {
|
|
128
|
+
setLoadError(resp?.error_message ?? 'Failed to read project');
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (!cancelled) setLoadError(String(e instanceof Error ? e.message : e));
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
}
|
|
135
|
+
return () => { cancelled = true; };
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
}, [visible, projectId, mode]);
|
|
138
|
+
|
|
139
|
+
// When the dialog closes, clear the loaded-marker so a subsequent
|
|
140
|
+
// open re-fetches fresh state.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!visible) lastLoadedIdRef.current = '';
|
|
143
|
+
}, [visible]);
|
|
144
|
+
|
|
145
|
+
const handleFieldChange = (field: TestFieldDef, val: any) => {
|
|
146
|
+
setValues(prev => ({ ...prev, [field.name]: val }));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// All required project_fields must be non-empty before we let the
|
|
150
|
+
// operator submit. Source-bound fields are validated the same way
|
|
151
|
+
// — they should already have a value from GM, but the dialog is
|
|
152
|
+
// honest about it if they don't.
|
|
153
|
+
const isValid = useMemo(() => {
|
|
154
|
+
for (const f of projectFields) {
|
|
155
|
+
if (!f.required) continue;
|
|
156
|
+
const v = values[f.name];
|
|
157
|
+
if (v === undefined || v === null || v === '') return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}, [projectFields, values]);
|
|
161
|
+
|
|
162
|
+
const handleSubmit = async () => {
|
|
163
|
+
if (!isValid || submitting) return;
|
|
164
|
+
setSubmitting(true);
|
|
165
|
+
try {
|
|
166
|
+
// For source-bound fields, push the value into GM in the
|
|
167
|
+
// same pass. We do this whether `create` or `edit` because
|
|
168
|
+
// the persisted project.json copy is just a snapshot —
|
|
169
|
+
// GM is the live source of truth and we want the rig to
|
|
170
|
+
// see the operator's changes immediately.
|
|
171
|
+
for (const f of projectFields) {
|
|
172
|
+
if (!f.source) continue;
|
|
173
|
+
const v = values[f.name];
|
|
174
|
+
if (v === undefined || v === null) continue;
|
|
175
|
+
try { await write(f.source, v); }
|
|
176
|
+
catch (e) { console.warn(`[ProjectInfoDialog] write to ${f.source} failed:`, e); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The persisted blob carries every project_field we
|
|
180
|
+
// present, including source-bound ones; this lets the
|
|
181
|
+
// form fold them into stage_test even if the live tag
|
|
182
|
+
// hasn't broadcast a value yet at staging time.
|
|
183
|
+
const projectFieldsPayload: Record<string, any> = {};
|
|
184
|
+
for (const f of projectFields) {
|
|
185
|
+
if (values[f.name] !== undefined) projectFieldsPayload[f.name] = values[f.name];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const topic = mode === 'create' ? 'tis.create_project' : 'tis.update_project';
|
|
189
|
+
const resp: any = await invoke(topic as any, MessageType.Request, {
|
|
190
|
+
project_id: projectId,
|
|
191
|
+
project_fields: projectFieldsPayload,
|
|
192
|
+
} as any);
|
|
193
|
+
if (resp?.success) {
|
|
194
|
+
onSubmitted(projectId, projectFieldsPayload);
|
|
195
|
+
onHide();
|
|
196
|
+
} else {
|
|
197
|
+
alert(`Failed: ${resp?.error_message ?? 'unknown error'}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
alert(`Failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
201
|
+
} finally {
|
|
202
|
+
setSubmitting(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const isFieldValid = (f: TestFieldDef) => {
|
|
207
|
+
if (!f.required) return true;
|
|
208
|
+
const v = values[f.name];
|
|
209
|
+
return v !== undefined && v !== null && v !== '';
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const renderField = (field: TestFieldDef) => {
|
|
213
|
+
const valid = isFieldValid(field);
|
|
214
|
+
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
215
|
+
const tooltipId = `acProjInfo_${field.name}`;
|
|
216
|
+
return (
|
|
217
|
+
<React.Fragment key={field.name}>
|
|
218
|
+
<span className="ac-form-label">{labelOf(field)}</span>
|
|
219
|
+
{isNum ? (
|
|
220
|
+
<ValueInput
|
|
221
|
+
label={undefined}
|
|
222
|
+
value={values[field.name] != null ? Number(values[field.name]) : null}
|
|
223
|
+
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
224
|
+
className={!valid ? 'p-invalid' : ''}
|
|
225
|
+
/>
|
|
226
|
+
) : (
|
|
227
|
+
<TextInput
|
|
228
|
+
label={undefined}
|
|
229
|
+
value={values[field.name] != null ? String(values[field.name]) : ''}
|
|
230
|
+
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
231
|
+
className={!valid ? 'p-invalid' : ''}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
{hasDescription(field) ? (
|
|
235
|
+
<>
|
|
236
|
+
<Tooltip target={`#${tooltipId}`} position="left" />
|
|
237
|
+
<span
|
|
238
|
+
id={tooltipId}
|
|
239
|
+
data-pr-tooltip={field.description}
|
|
240
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'help' }}
|
|
241
|
+
>
|
|
242
|
+
<i className="pi pi-info-circle" style={{ color: 'var(--text-secondary-color)' }} />
|
|
243
|
+
</span>
|
|
244
|
+
</>
|
|
245
|
+
) : (
|
|
246
|
+
<span aria-hidden="true" />
|
|
247
|
+
)}
|
|
248
|
+
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
249
|
+
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
250
|
+
</span>
|
|
251
|
+
</React.Fragment>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const footer = (
|
|
256
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
|
|
257
|
+
<Button label="Cancel" icon="pi pi-times" onClick={onHide} disabled={submitting} text />
|
|
258
|
+
<Button
|
|
259
|
+
label={mode === 'create' ? 'Create Project' : 'Save'}
|
|
260
|
+
icon={submitting ? 'pi pi-spin pi-spinner' : (mode === 'create' ? 'pi pi-plus' : 'pi pi-check')}
|
|
261
|
+
onClick={handleSubmit}
|
|
262
|
+
disabled={!isValid || submitting}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const header = mode === 'create'
|
|
268
|
+
? `Create project: ${projectId || '(no ID)'}`
|
|
269
|
+
: `Edit project information: ${projectId}`;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Dialog
|
|
273
|
+
header={header}
|
|
274
|
+
visible={visible}
|
|
275
|
+
onHide={onHide}
|
|
276
|
+
footer={footer}
|
|
277
|
+
modal
|
|
278
|
+
style={{ width: 'min(640px, 90vw)' }}
|
|
279
|
+
// PrimeReact's `closable` X — same as Cancel.
|
|
280
|
+
closable={!submitting}
|
|
281
|
+
>
|
|
282
|
+
{loadError && (
|
|
283
|
+
<div style={{ color: 'var(--red-500)', marginBottom: '1rem' }}>
|
|
284
|
+
{loadError}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
{projectFields.length === 0 ? (
|
|
288
|
+
<p style={{ color: 'var(--text-secondary-color)' }}>
|
|
289
|
+
{mode === 'create'
|
|
290
|
+
? `Click "Create Project" to create the empty project "${projectId}". `
|
|
291
|
+
+ `This method declares no project_fields, so there's nothing to fill in.`
|
|
292
|
+
: `This method declares no project_fields, so there's nothing to edit.`}
|
|
293
|
+
</p>
|
|
294
|
+
) : (
|
|
295
|
+
<div
|
|
296
|
+
className="ac-form-grid"
|
|
297
|
+
style={{
|
|
298
|
+
padding: '0.25rem 0',
|
|
299
|
+
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
{projectFields.map(renderField)}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</Dialog>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
@@ -65,6 +65,10 @@ export interface TestMethod {
|
|
|
65
65
|
results_fields: TestFieldDef[];
|
|
66
66
|
raw_data?: RawDataShape | null;
|
|
67
67
|
views?: { [name: string]: ChartView };
|
|
68
|
+
/** Optional pretty label for the Test Method picker. */
|
|
69
|
+
label?: string;
|
|
70
|
+
/** Optional long-form description for the picker. */
|
|
71
|
+
description?: string;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export interface TestDataViewProps {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <TestMethodDialog> — picker UI for swapping the active test method.
|
|
5
|
+
*
|
|
6
|
+
* The main form displays a single "Test Method: <label>" row with an
|
|
7
|
+
* edit button. Clicking edit opens this dialog. The dialog shows a
|
|
8
|
+
* dropdown of every method declared in the current project's
|
|
9
|
+
* `test_methods` block, plus the long-form description for whichever
|
|
10
|
+
* method the operator has the dropdown highlighted on. OK commits the
|
|
11
|
+
* choice via the supplied callback; Cancel discards.
|
|
12
|
+
*
|
|
13
|
+
* This pattern scales past three or four methods where a SelectButton
|
|
14
|
+
* starts wrapping or eating horizontal space, and gives every method
|
|
15
|
+
* room to ship a description that disambiguates similar names.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { Button } from 'primereact/button';
|
|
20
|
+
import { Dialog } from 'primereact/dialog';
|
|
21
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
22
|
+
import type { TestMethod } from './TestSetupForm';
|
|
23
|
+
import { useTisSchemas } from './TisProvider';
|
|
24
|
+
|
|
25
|
+
export interface TestMethodDialogProps {
|
|
26
|
+
visible: boolean;
|
|
27
|
+
onHide: () => void;
|
|
28
|
+
/** Method ID currently selected on the form. The dropdown opens
|
|
29
|
+
* pointing at this value so the dialog reflects current state. */
|
|
30
|
+
currentMethodId: string;
|
|
31
|
+
/**
|
|
32
|
+
* Called with the chosen method_id when the operator clicks OK.
|
|
33
|
+
* Cancel does not fire this callback. The parent is responsible
|
|
34
|
+
* for actually applying the new selection (e.g., updating the
|
|
35
|
+
* provider's selection or local state).
|
|
36
|
+
*/
|
|
37
|
+
onSelected: (methodId: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Display name for one method: prefer the schema's `label`, fall
|
|
41
|
+
* back to the canonical `method_id` key. */
|
|
42
|
+
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
43
|
+
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
44
|
+
|
|
45
|
+
export const TestMethodDialog: React.FC<TestMethodDialogProps> = ({
|
|
46
|
+
visible, onHide, currentMethodId, onSelected,
|
|
47
|
+
}) => {
|
|
48
|
+
const schemas = useTisSchemas();
|
|
49
|
+
|
|
50
|
+
// Local "draft" selection — the dropdown writes to this; OK
|
|
51
|
+
// applies it. We deliberately don't update the provider's
|
|
52
|
+
// selection on every dropdown change, so a Cancel really cancels.
|
|
53
|
+
const [draftMethodId, setDraftMethodId] = useState<string>(currentMethodId);
|
|
54
|
+
|
|
55
|
+
// Re-sync the draft whenever the dialog opens so a stale value
|
|
56
|
+
// from a previous open doesn't ghost the current selection.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (visible) setDraftMethodId(currentMethodId);
|
|
59
|
+
}, [visible, currentMethodId]);
|
|
60
|
+
|
|
61
|
+
const options = useMemo(() => {
|
|
62
|
+
return Object.keys(schemas).map(methodId => ({
|
|
63
|
+
label: methodLabelOf(methodId, schemas[methodId]),
|
|
64
|
+
value: methodId,
|
|
65
|
+
}));
|
|
66
|
+
}, [schemas]);
|
|
67
|
+
|
|
68
|
+
const draftSchema = schemas[draftMethodId];
|
|
69
|
+
const draftDescription =
|
|
70
|
+
(draftSchema?.description && draftSchema.description.length > 0)
|
|
71
|
+
? draftSchema.description
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
const handleOk = () => {
|
|
75
|
+
if (draftMethodId && draftMethodId !== currentMethodId) {
|
|
76
|
+
onSelected(draftMethodId);
|
|
77
|
+
}
|
|
78
|
+
onHide();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const footer = (
|
|
82
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
|
|
83
|
+
<Button label="Cancel" icon="pi pi-times" onClick={onHide} text />
|
|
84
|
+
<Button
|
|
85
|
+
label="OK"
|
|
86
|
+
icon="pi pi-check"
|
|
87
|
+
onClick={handleOk}
|
|
88
|
+
disabled={!draftMethodId}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Dialog
|
|
95
|
+
header="Select Test Method"
|
|
96
|
+
visible={visible}
|
|
97
|
+
onHide={onHide}
|
|
98
|
+
footer={footer}
|
|
99
|
+
modal
|
|
100
|
+
style={{ width: 'min(560px, 90vw)' }}
|
|
101
|
+
>
|
|
102
|
+
{options.length === 0 ? (
|
|
103
|
+
<p style={{ color: 'var(--text-secondary-color)' }}>
|
|
104
|
+
No test methods are declared in this project's <code>test_methods</code> block.
|
|
105
|
+
</p>
|
|
106
|
+
) : (
|
|
107
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
108
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
109
|
+
<label htmlFor="acTestMethodDropdown" style={{ flexShrink: 0 }}>
|
|
110
|
+
Test Method:
|
|
111
|
+
</label>
|
|
112
|
+
<Dropdown
|
|
113
|
+
inputId="acTestMethodDropdown"
|
|
114
|
+
value={draftMethodId}
|
|
115
|
+
options={options}
|
|
116
|
+
onChange={(e) => setDraftMethodId(e.value)}
|
|
117
|
+
placeholder="Select a method"
|
|
118
|
+
style={{ flex: 1 }}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
{/*
|
|
122
|
+
* Show the description region whenever we have a
|
|
123
|
+
* draft selection — keeps layout stable even when
|
|
124
|
+
* the operator picks a method without one (we just
|
|
125
|
+
* render a muted placeholder rather than collapsing
|
|
126
|
+
* the dialog by ~3rem).
|
|
127
|
+
*/}
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
padding: '0.75rem 1rem',
|
|
131
|
+
background: 'var(--surface-100)',
|
|
132
|
+
borderRadius: '6px',
|
|
133
|
+
minHeight: '4.5rem',
|
|
134
|
+
color: draftDescription
|
|
135
|
+
? 'var(--text-color)'
|
|
136
|
+
: 'var(--text-secondary-color)',
|
|
137
|
+
fontStyle: draftDescription ? 'normal' : 'italic',
|
|
138
|
+
whiteSpace: 'pre-wrap',
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{draftDescription ?? 'No description provided for this test method.'}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</Dialog>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
|
|
2
2
|
import { AutoComplete } from 'primereact/autocomplete';
|
|
3
3
|
import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
|
|
4
|
-
import {
|
|
4
|
+
import { Button } from 'primereact/button';
|
|
5
|
+
import { InputText } from 'primereact/inputtext';
|
|
6
|
+
import { Tooltip } from 'primereact/tooltip';
|
|
5
7
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
8
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
7
9
|
import { MessageType } from '../../hub/CommandMessage';
|
|
8
10
|
import { ValueInput } from '../ValueInput';
|
|
9
11
|
import { TextInput } from '../TextInput';
|
|
10
12
|
import { useTis } from './TisProvider';
|
|
13
|
+
import { ProjectInfoDialog } from './ProjectInfoDialog';
|
|
14
|
+
import { TestMethodDialog } from './TestMethodDialog';
|
|
11
15
|
|
|
12
16
|
export interface TestFieldDef {
|
|
17
|
+
/** Canonical key — wire format, generated code, on-disk JSON. */
|
|
13
18
|
name: string;
|
|
14
19
|
type: string;
|
|
15
20
|
units?: string;
|
|
16
21
|
required?: boolean;
|
|
17
22
|
source?: string;
|
|
23
|
+
/** Pretty label rendered by the form. Falls back to `name`. Units
|
|
24
|
+
* are appended automatically; don't pre-format `[mm]` into label. */
|
|
25
|
+
label?: string;
|
|
26
|
+
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
27
|
+
description?: string;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export interface TestMethod {
|
|
@@ -22,18 +32,17 @@ export interface TestMethod {
|
|
|
22
32
|
config_fields: TestFieldDef[];
|
|
23
33
|
cycle_fields: TestFieldDef[];
|
|
24
34
|
results_fields: TestFieldDef[];
|
|
35
|
+
/** Optional pretty label for the Test Method picker. Falls back
|
|
36
|
+
* to the canonical method_id key. */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Optional long-form description shown in the picker dialog
|
|
39
|
+
* when this method is highlighted. */
|
|
40
|
+
description?: string;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
/**
|
|
28
44
|
* Props are all optional overrides — by default the form drives itself
|
|
29
|
-
* from the surrounding `<TisProvider>`.
|
|
30
|
-
* particular axis.
|
|
31
|
-
*
|
|
32
|
-
* - `schema`: bypass `useTisSchemas()` for the selected method.
|
|
33
|
-
* - `defaultProjectId` / `defaultMethodId`: seed the form's local
|
|
34
|
-
* state when the corresponding TIS-context fields are blank.
|
|
35
|
-
* - `onProjectChange` / `onMethodChange` / `onValidationChange`:
|
|
36
|
-
* receive callbacks in addition to the provider's selection state.
|
|
45
|
+
* from the surrounding `<TisProvider>`.
|
|
37
46
|
*/
|
|
38
47
|
export interface TestSetupFormProps {
|
|
39
48
|
schema?: TestMethod;
|
|
@@ -44,6 +53,32 @@ export interface TestSetupFormProps {
|
|
|
44
53
|
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
// Helpers
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const labelOf = (f: TestFieldDef): string => {
|
|
61
|
+
const base = f.label && f.label.length > 0 ? f.label : f.name;
|
|
62
|
+
return f.units ? `${base} [${f.units}]` : base;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const hasDescription = (f: TestFieldDef): boolean =>
|
|
66
|
+
typeof f.description === 'string' && f.description.length > 0;
|
|
67
|
+
|
|
68
|
+
/** Display name for one method: prefer schema's `label`, fall back
|
|
69
|
+
* to the canonical method_id. Mirrors the helper in TestMethodDialog
|
|
70
|
+
* so the row label stays in sync with what the dialog shows. */
|
|
71
|
+
const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
|
|
72
|
+
(schema?.label && schema.label.length > 0) ? schema.label : methodId;
|
|
73
|
+
|
|
74
|
+
// Project IDs follow the same character class as the server's
|
|
75
|
+
// `tis.create_project` validator. Keep these in sync — see
|
|
76
|
+
// `src/tis_servelet.rs::create_project`.
|
|
77
|
+
const PROJECT_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
78
|
+
const isValidProjectIdFormat = (id: string) => PROJECT_ID_RE.test(id);
|
|
79
|
+
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
|
|
47
82
|
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
48
83
|
schema: schemaOverride,
|
|
49
84
|
defaultProjectId,
|
|
@@ -58,9 +93,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
58
93
|
|
|
59
94
|
const methodIds = useMemo(() => Object.keys(tis.schemas), [tis.schemas]);
|
|
60
95
|
|
|
61
|
-
// Seed the form's local project/method state from the provider's
|
|
62
|
-
// selection. Direct edits update the provider via setSelection so
|
|
63
|
-
// the History tab and Data tab follow along.
|
|
64
96
|
const [projectId, setProjectIdLocal] = useState<string>(
|
|
65
97
|
tis.selection.projectId || defaultProjectId || ''
|
|
66
98
|
);
|
|
@@ -70,13 +102,8 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
70
102
|
const [sampleId, setSampleIdLocal] = useState<string>('');
|
|
71
103
|
const [config, setConfig] = useState<any>({});
|
|
72
104
|
|
|
73
|
-
// Resolve the schema for the active method. The override beats the
|
|
74
|
-
// registry; if neither is available, render the empty state.
|
|
75
105
|
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
76
106
|
|
|
77
|
-
// Push local edits back into the provider's selection so other
|
|
78
|
-
// components react. Only push when the value actually differs to
|
|
79
|
-
// avoid feedback loops.
|
|
80
107
|
useEffect(() => {
|
|
81
108
|
if (tis.selection.projectId !== projectId) {
|
|
82
109
|
tis.setSelection({ projectId });
|
|
@@ -100,10 +127,6 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
100
127
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
128
|
}, [sampleId]);
|
|
102
129
|
|
|
103
|
-
// Track the live broadcast scalars — when the form is staged for
|
|
104
|
-
// a different sample externally (e.g., the operator types in
|
|
105
|
-
// another tab), reflect that here. Only react on a true change
|
|
106
|
-
// to avoid stomping local edits.
|
|
107
130
|
useEffect(() => {
|
|
108
131
|
if (tis.state.stagedSampleId && tis.state.stagedSampleId !== sampleId) {
|
|
109
132
|
setSampleIdLocal(tis.state.stagedSampleId);
|
|
@@ -113,16 +136,90 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
113
136
|
|
|
114
137
|
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
115
138
|
const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
|
|
139
|
+
const justCreatedRef = useRef<Set<string>>(new Set());
|
|
140
|
+
const [justCreatedTick, setJustCreatedTick] = useState(0);
|
|
116
141
|
const [isValid, setIsValid] = useState(false);
|
|
117
142
|
|
|
118
|
-
//
|
|
143
|
+
// Cache of `project_fields` for the currently-selected project,
|
|
144
|
+
// fetched once on project selection. Folded into every
|
|
145
|
+
// `tis.stage_test` payload so the recorded test.json carries the
|
|
146
|
+
// project-level metadata even though the operator no longer sees
|
|
147
|
+
// those fields in the main form. Keyed by projectId so switching
|
|
148
|
+
// projects mid-session refetches cleanly.
|
|
149
|
+
const [projectFieldsCache, setProjectFieldsCache] = useState<Record<string, any>>({});
|
|
150
|
+
const projectFieldsForCurrent = projectFieldsCache[projectId] ?? {};
|
|
151
|
+
|
|
152
|
+
// Dialog state for create + edit + method-picker.
|
|
153
|
+
const [newProjectOpen, setNewProjectOpen] = useState(false);
|
|
154
|
+
const [editProjectOpen, setEditProjectOpen] = useState(false);
|
|
155
|
+
const [methodPickerOpen, setMethodPickerOpen] = useState(false);
|
|
156
|
+
|
|
157
|
+
const fetchProjects = async () => {
|
|
158
|
+
try {
|
|
159
|
+
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
|
|
160
|
+
if (resp.success && resp.data && resp.data.projects) {
|
|
161
|
+
setExistingProjects(resp.data.projects);
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('Failed to list projects', err);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
fetchProjects();
|
|
170
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
|
+
}, [invoke]);
|
|
172
|
+
|
|
173
|
+
const knownProjects = useMemo(() => {
|
|
174
|
+
const s = new Set<string>(existingProjects);
|
|
175
|
+
for (const id of justCreatedRef.current) s.add(id);
|
|
176
|
+
return s;
|
|
177
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
+
}, [existingProjects, justCreatedTick]);
|
|
179
|
+
|
|
180
|
+
const projectExists = projectId.trim() !== '' && knownProjects.has(projectId.trim());
|
|
181
|
+
const projectIdFormatValid = isValidProjectIdFormat(projectId.trim());
|
|
182
|
+
const canCreateProject =
|
|
183
|
+
projectId.trim() !== ''
|
|
184
|
+
&& projectIdFormatValid
|
|
185
|
+
&& !knownProjects.has(projectId.trim());
|
|
186
|
+
|
|
187
|
+
// Whenever the user selects a known project (or the project
|
|
188
|
+
// gets created in-session), pull its persisted project_fields
|
|
189
|
+
// so we can fold them into stage_test. We don't refetch on
|
|
190
|
+
// every keystroke — only when projectId actually lands on an
|
|
191
|
+
// existing project we don't yet have cached.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const pid = projectId.trim();
|
|
194
|
+
if (!pid || !projectExists) return;
|
|
195
|
+
if (projectFieldsCache[pid] !== undefined) return; // already cached
|
|
196
|
+
let cancelled = false;
|
|
197
|
+
(async () => {
|
|
198
|
+
try {
|
|
199
|
+
const resp: any = await invoke(
|
|
200
|
+
'tis.read_project' as any, MessageType.Request,
|
|
201
|
+
{ project_id: pid } as any,
|
|
202
|
+
);
|
|
203
|
+
if (cancelled) return;
|
|
204
|
+
if (resp?.success) {
|
|
205
|
+
const pf = (resp.data?.project_fields ?? {}) as Record<string, any>;
|
|
206
|
+
setProjectFieldsCache(prev => ({ ...prev, [pid]: pf }));
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.warn('[TestSetupForm] read_project failed:', e);
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
return () => { cancelled = true; };
|
|
213
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
+
}, [projectId, projectExists]);
|
|
215
|
+
|
|
216
|
+
// Seed and live-update config_fields that declare a `source`.
|
|
119
217
|
useEffect(() => {
|
|
120
218
|
if (!schema) return;
|
|
121
|
-
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
122
219
|
setConfig((prev: any) => {
|
|
123
220
|
let next = prev;
|
|
124
|
-
for (const field of
|
|
125
|
-
if (field.name === 'sample_id') continue;
|
|
221
|
+
for (const field of schema.config_fields) {
|
|
222
|
+
if (field.name === 'sample_id') continue;
|
|
126
223
|
if (!field.source) continue;
|
|
127
224
|
const tag = findTagByFqdn(field.source);
|
|
128
225
|
if (!tag) continue;
|
|
@@ -136,61 +233,64 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
136
233
|
});
|
|
137
234
|
}, [schema, rawValues, findTagByFqdn]);
|
|
138
235
|
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
const fetchProjects = async () => {
|
|
141
|
-
try {
|
|
142
|
-
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
|
|
143
|
-
if (resp.success && resp.data && resp.data.projects) {
|
|
144
|
-
setExistingProjects(resp.data.projects);
|
|
145
|
-
}
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error('Failed to list projects', err);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
fetchProjects();
|
|
151
|
-
}, [invoke]);
|
|
152
|
-
|
|
153
236
|
const searchProjects = (event: AutoCompleteCompleteEvent) => {
|
|
154
237
|
const query = event.query.toLowerCase();
|
|
155
238
|
setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
|
|
156
239
|
};
|
|
157
240
|
|
|
158
|
-
// Validation drives both the local UI and the auto-stage.
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
241
|
+
// Validation drives both the local UI and the auto-stage. Project
|
|
242
|
+
// ID must be a known project — typing an unknown name is invalid
|
|
243
|
+
// until the operator goes through the New Project dialog. Each
|
|
244
|
+
// required *config_field* must also be filled in; project_fields
|
|
245
|
+
// are validated by the dialog at create/edit time, not here.
|
|
162
246
|
useEffect(() => {
|
|
163
247
|
if (!schema) { setIsValid(false); return; }
|
|
164
248
|
let valid = true;
|
|
165
|
-
if (!
|
|
249
|
+
if (!projectExists) valid = false;
|
|
166
250
|
if (!methodId.trim()) valid = false;
|
|
167
251
|
if (!sampleId.trim()) valid = false;
|
|
168
252
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
if (field.name === 'sample_id') continue; // tracked separately
|
|
253
|
+
for (const field of schema.config_fields) {
|
|
254
|
+
if (field.name === 'sample_id') continue;
|
|
172
255
|
if (field.required) {
|
|
173
256
|
const v = config[field.name];
|
|
174
257
|
if (v === undefined || v === '' || v === null) { valid = false; break; }
|
|
175
258
|
}
|
|
176
259
|
}
|
|
260
|
+
|
|
261
|
+
// We also gate validity on having loaded the project_fields
|
|
262
|
+
// for the selected project — staging *without* them would
|
|
263
|
+
// record a test.json that's missing project-level metadata
|
|
264
|
+
// for the lifetime of the run. The fetch happens automatically
|
|
265
|
+
// when the project is selected, so this is a tight transient
|
|
266
|
+
// window in practice.
|
|
267
|
+
if (valid && projectExists && projectFieldsCache[projectId.trim()] === undefined) {
|
|
268
|
+
valid = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
177
271
|
setIsValid(valid);
|
|
178
272
|
if (onValidationChange) onValidationChange(valid, config);
|
|
179
273
|
|
|
180
|
-
// Auto-stage when everything is valid. Stripping sample_id from
|
|
181
|
-
// config (it's a top-level peer of project_id/method_id now;
|
|
182
|
-
// the legacy nested form is server-side hoisted with a
|
|
183
|
-
// deprecation warning, but new code shouldn't rely on it).
|
|
184
274
|
if (valid) {
|
|
185
|
-
const { sample_id: _drop, ...
|
|
275
|
+
const { sample_id: _drop, ...configRest } = (config ?? {}) as any;
|
|
276
|
+
// Combine persisted project_fields (managerial setup) with
|
|
277
|
+
// the per-test config_fields the operator just filled in.
|
|
278
|
+
// Keys collide in pathological project.json, in which case
|
|
279
|
+
// the per-test value wins — operators are closer to the
|
|
280
|
+
// run than the project metadata.
|
|
281
|
+
const mergedConfig = {
|
|
282
|
+
...projectFieldsForCurrent,
|
|
283
|
+
...configRest,
|
|
284
|
+
};
|
|
186
285
|
void invoke('tis.stage_test' as any, MessageType.Request, {
|
|
187
286
|
project_id: projectId,
|
|
188
287
|
method_id: methodId,
|
|
189
288
|
sample_id: sampleId,
|
|
190
|
-
config:
|
|
289
|
+
config: mergedConfig,
|
|
191
290
|
} as any).catch(e => console.error('[TestSetupForm] stage_test failed:', e));
|
|
192
291
|
}
|
|
193
|
-
|
|
292
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
293
|
+
}, [config, schema, projectId, methodId, sampleId, projectExists, projectFieldsCache, onValidationChange, invoke]);
|
|
194
294
|
|
|
195
295
|
const isFieldValid = (field: TestFieldDef) => {
|
|
196
296
|
if (!field.required) return true;
|
|
@@ -199,7 +299,7 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
199
299
|
};
|
|
200
300
|
|
|
201
301
|
const handleProjectIdChange = (value: string | null | undefined) => {
|
|
202
|
-
const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
|
|
302
|
+
const sanitized = (value || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
203
303
|
setProjectIdLocal(sanitized);
|
|
204
304
|
};
|
|
205
305
|
|
|
@@ -215,13 +315,34 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
215
315
|
}
|
|
216
316
|
};
|
|
217
317
|
|
|
218
|
-
|
|
318
|
+
// Called by ProjectInfoDialog after a successful create/update.
|
|
319
|
+
// Populates the local cache + known-projects set so the main form
|
|
320
|
+
// is immediately valid without requiring a refresh.
|
|
321
|
+
const handleProjectInfoSubmitted = (pid: string, projectFields: Record<string, any>) => {
|
|
322
|
+
justCreatedRef.current.add(pid);
|
|
323
|
+
setJustCreatedTick(t => t + 1);
|
|
324
|
+
setProjectFieldsCache(prev => ({ ...prev, [pid]: projectFields }));
|
|
325
|
+
// Refresh the dropdown so the new project shows up for any
|
|
326
|
+
// future searches in this session.
|
|
327
|
+
void fetchProjects();
|
|
328
|
+
// If the dialog created a brand-new project, also surface it
|
|
329
|
+
// as the current selection — the operator's intent is clearly
|
|
330
|
+
// "set up this project and start working on it."
|
|
331
|
+
if (projectId.trim() !== pid) setProjectIdLocal(pid);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// -----------------------------------------------------------------
|
|
335
|
+
// Per-config-field row renderer — same four-column layout as before:
|
|
336
|
+
// label[units] | input | info-icon | validity-icon
|
|
337
|
+
// -----------------------------------------------------------------
|
|
338
|
+
const renderConfigField = (field: TestFieldDef) => {
|
|
219
339
|
if (field.name === 'sample_id') return null;
|
|
220
340
|
const valid = isFieldValid(field);
|
|
221
341
|
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
342
|
+
const tooltipId = `acFormInfo_${field.name}`;
|
|
222
343
|
return (
|
|
223
344
|
<React.Fragment key={field.name}>
|
|
224
|
-
<span className="ac-form-label">{field
|
|
345
|
+
<span className="ac-form-label">{labelOf(field)}</span>
|
|
225
346
|
{isNum ? (
|
|
226
347
|
<ValueInput
|
|
227
348
|
label={undefined}
|
|
@@ -237,7 +358,20 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
237
358
|
className={!valid ? 'p-invalid' : ''}
|
|
238
359
|
/>
|
|
239
360
|
)}
|
|
240
|
-
|
|
361
|
+
{hasDescription(field) ? (
|
|
362
|
+
<>
|
|
363
|
+
<Tooltip target={`#${tooltipId}`} position="left" />
|
|
364
|
+
<span
|
|
365
|
+
id={tooltipId}
|
|
366
|
+
data-pr-tooltip={field.description}
|
|
367
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'help' }}
|
|
368
|
+
>
|
|
369
|
+
<i className="pi pi-info-circle" style={{ color: 'var(--text-secondary-color)' }} />
|
|
370
|
+
</span>
|
|
371
|
+
</>
|
|
372
|
+
) : (
|
|
373
|
+
<span aria-hidden="true" />
|
|
374
|
+
)}
|
|
241
375
|
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
242
376
|
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
243
377
|
</span>
|
|
@@ -255,8 +389,15 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
255
389
|
);
|
|
256
390
|
}
|
|
257
391
|
|
|
392
|
+
const gridStyle: React.CSSProperties = {
|
|
393
|
+
padding: '1.25rem',
|
|
394
|
+
gridTemplateColumns: 'auto 1fr 1.75rem 1.75rem',
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const projectRowValid = projectExists;
|
|
398
|
+
|
|
258
399
|
return (
|
|
259
|
-
<div className="ac-form-grid" style={
|
|
400
|
+
<div className="ac-form-grid" style={gridStyle}>
|
|
260
401
|
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
261
402
|
Project & Method
|
|
262
403
|
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
@@ -265,18 +406,62 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
265
406
|
</h3>
|
|
266
407
|
|
|
267
408
|
<span className="ac-form-label">Project ID</span>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
409
|
+
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
410
|
+
<AutoComplete
|
|
411
|
+
value={projectId}
|
|
412
|
+
suggestions={filteredProjects}
|
|
413
|
+
completeMethod={searchProjects}
|
|
414
|
+
onChange={(e) => handleProjectIdChange(e.value)}
|
|
415
|
+
dropdown
|
|
416
|
+
placeholder="Select an existing Project ID, or type a new one and click +"
|
|
417
|
+
className={!projectRowValid ? 'p-invalid' : ''}
|
|
418
|
+
style={{ flex: 1 }}
|
|
419
|
+
/>
|
|
420
|
+
{/*
|
|
421
|
+
* + button → opens the New Project dialog where the
|
|
422
|
+
* operator (or manager) fills in project_fields.
|
|
423
|
+
* Enabled only when the typed ID is a fresh, valid
|
|
424
|
+
* candidate. Once the dialog completes, the project
|
|
425
|
+
* is created on the server and added to our local
|
|
426
|
+
* known-projects set so the main form becomes valid
|
|
427
|
+
* immediately.
|
|
428
|
+
*/}
|
|
429
|
+
<Button
|
|
430
|
+
icon="pi pi-plus"
|
|
431
|
+
type="button"
|
|
432
|
+
onClick={() => setNewProjectOpen(true)}
|
|
433
|
+
disabled={!canCreateProject}
|
|
434
|
+
tooltip={
|
|
435
|
+
!projectId.trim() ? 'Type a project ID first' :
|
|
436
|
+
!projectIdFormatValid ? 'Letters, digits, _ and - only' :
|
|
437
|
+
knownProjects.has(projectId.trim()) ? 'Project already exists' :
|
|
438
|
+
`Create project "${projectId.trim()}"`
|
|
439
|
+
}
|
|
440
|
+
tooltipOptions={{ position: 'top' }}
|
|
441
|
+
/>
|
|
442
|
+
{/*
|
|
443
|
+
* ✏️ button → opens the Edit Project Information
|
|
444
|
+
* dialog. Enabled only when the selected project
|
|
445
|
+
* actually exists. This is the only way to mutate
|
|
446
|
+
* project_fields after creation; the operator can't
|
|
447
|
+
* stumble into editing project metadata while running
|
|
448
|
+
* a sample, which keeps the future per-user permission
|
|
449
|
+
* gate (manager vs operator) clean.
|
|
450
|
+
*/}
|
|
451
|
+
<Button
|
|
452
|
+
icon="pi pi-pencil"
|
|
453
|
+
type="button"
|
|
454
|
+
onClick={() => setEditProjectOpen(true)}
|
|
455
|
+
disabled={!projectExists}
|
|
456
|
+
tooltip={projectExists
|
|
457
|
+
? `Edit information for "${projectId.trim()}"`
|
|
458
|
+
: 'Select an existing project to edit'}
|
|
459
|
+
tooltipOptions={{ position: 'top' }}
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
<span aria-hidden="true" />
|
|
463
|
+
<span style={{ color: projectRowValid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
464
|
+
<i className={projectRowValid ? 'pi pi-check' : 'pi pi-times'} />
|
|
280
465
|
</span>
|
|
281
466
|
|
|
282
467
|
<span className="ac-form-label">Sample ID</span>
|
|
@@ -286,29 +471,82 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
286
471
|
onValueChanged={handleSampleIdChange}
|
|
287
472
|
className={!sampleId.trim() ? 'p-invalid' : ''}
|
|
288
473
|
/>
|
|
289
|
-
<span />
|
|
474
|
+
<span aria-hidden="true" />
|
|
290
475
|
<span style={{ color: sampleId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
291
476
|
<i className={sampleId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
292
477
|
</span>
|
|
293
478
|
|
|
294
|
-
{
|
|
479
|
+
{/*
|
|
480
|
+
* Test Method row. Shows the current method's pretty
|
|
481
|
+
* label (or its canonical method_id when no label is
|
|
482
|
+
* declared) read-only, with an edit button that opens
|
|
483
|
+
* the picker dialog. The dialog scales past three or
|
|
484
|
+
* four methods where a SelectButton would wrap, and
|
|
485
|
+
* surfaces the per-method description so the operator
|
|
486
|
+
* can disambiguate similarly-named methods at the
|
|
487
|
+
* point of choice. The row is rendered even when only
|
|
488
|
+
* one method is declared so the operator can still open
|
|
489
|
+
* the picker and read its description.
|
|
490
|
+
*/}
|
|
491
|
+
{methodIds.length > 0 && (
|
|
295
492
|
<>
|
|
296
493
|
<span className="ac-form-label">Test Method</span>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
494
|
+
<div className="p-inputgroup" style={{ flex: 1 }}>
|
|
495
|
+
<InputText
|
|
496
|
+
value={methodLabelOf(methodId, schema)}
|
|
497
|
+
readOnly
|
|
498
|
+
style={{ flex: 1 }}
|
|
499
|
+
tabIndex={-1}
|
|
500
|
+
/>
|
|
501
|
+
<Button
|
|
502
|
+
icon="pi pi-pencil"
|
|
503
|
+
type="button"
|
|
504
|
+
onClick={() => setMethodPickerOpen(true)}
|
|
505
|
+
tooltip={methodIds.length > 1
|
|
506
|
+
? 'Change test method'
|
|
507
|
+
: 'View test method details'}
|
|
508
|
+
tooltipOptions={{ position: 'top' }}
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
<span aria-hidden="true" />
|
|
512
|
+
<span style={{ color: methodId ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
513
|
+
<i className={methodId ? 'pi pi-check' : 'pi pi-times'} />
|
|
514
|
+
</span>
|
|
304
515
|
</>
|
|
305
516
|
)}
|
|
306
517
|
|
|
307
|
-
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
|
|
308
|
-
{schema.project_fields.map(renderField)}
|
|
309
|
-
|
|
310
518
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
311
|
-
{schema.config_fields.map(
|
|
519
|
+
{schema.config_fields.map(renderConfigField)}
|
|
520
|
+
|
|
521
|
+
{/*
|
|
522
|
+
* Project Information no longer renders inline. Use the
|
|
523
|
+
* + and ✏️ buttons in the Project ID row above to create
|
|
524
|
+
* or edit it. The dialogs persist project_fields on the
|
|
525
|
+
* server and the form folds them into stage_test
|
|
526
|
+
* automatically.
|
|
527
|
+
*/}
|
|
528
|
+
<ProjectInfoDialog
|
|
529
|
+
visible={newProjectOpen}
|
|
530
|
+
onHide={() => setNewProjectOpen(false)}
|
|
531
|
+
mode="create"
|
|
532
|
+
projectId={projectId.trim()}
|
|
533
|
+
projectFields={schema.project_fields}
|
|
534
|
+
onSubmitted={handleProjectInfoSubmitted}
|
|
535
|
+
/>
|
|
536
|
+
<ProjectInfoDialog
|
|
537
|
+
visible={editProjectOpen}
|
|
538
|
+
onHide={() => setEditProjectOpen(false)}
|
|
539
|
+
mode="edit"
|
|
540
|
+
projectId={projectId.trim()}
|
|
541
|
+
projectFields={schema.project_fields}
|
|
542
|
+
onSubmitted={handleProjectInfoSubmitted}
|
|
543
|
+
/>
|
|
544
|
+
<TestMethodDialog
|
|
545
|
+
visible={methodPickerOpen}
|
|
546
|
+
onHide={() => setMethodPickerOpen(false)}
|
|
547
|
+
currentMethodId={methodId}
|
|
548
|
+
onSelected={(picked) => setMethodIdLocal(picked)}
|
|
549
|
+
/>
|
|
312
550
|
</div>
|
|
313
551
|
);
|
|
314
552
|
};
|