@adcops/autocore-react 3.3.54 → 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/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/hub/HubWebSocket.d.ts +13 -0
- package/dist/hub/HubWebSocket.d.ts.map +1 -1
- package/dist/hub/HubWebSocket.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
- package/src/components/tis/TisProvider.tsx +3 -2
- package/src/hub/HubWebSocket.ts +66 -3
|
@@ -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)})]})};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,EASV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;gDACgD;AAChD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,cAAc,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IAEvB,KAAK,EAAE,YAAY,CAAC;IAEpB,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAEjD;;qCAEiC;IACjC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpE;uEACmE;IACnE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnG,QAAQ,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAA;KAAE,CAAC;CACnD;AAeD,QAAA,MAAM,UAAU,gCAUd,CAAC;AAmCH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,EASV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;gDACgD;AAChD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,cAAc,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IAEvB,KAAK,EAAE,YAAY,CAAC;IAEpB,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAEjD;;qCAEiC;IACjC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpE;uEACmE;IACnE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnG,QAAQ,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAA;KAAE,CAAC;CACnD;AAeD,QAAA,MAAM,UAAU,gCAUd,CAAC;AAmCH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAmLlD,CAAC;AAMF,eAAO,MAAM,MAAM,uBAAyC,CAAC;AAC7D,eAAO,MAAM,aAAa,sBAA0C,CAAC;AACrE,eAAO,MAAM,WAAW,oBAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,wCA5QF,iBAAiB,KAAK,IAAI,CA+QnD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,EAAE,WAAW,MAAM;;;;CAY/D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;;;;;;CAuBvC,CAAC;AAIF,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -30,6 +30,19 @@ export declare class HubWebSocket extends HubBase {
|
|
|
30
30
|
* Used to match asynchronous WebSocket responses to their original requests.
|
|
31
31
|
*/
|
|
32
32
|
private pendingRequests;
|
|
33
|
+
/**
|
|
34
|
+
* Messages queued while the WebSocket is still in CONNECTING state.
|
|
35
|
+
* Flushed in `socket.onopen`. Without this queue, an `invoke()` fired
|
|
36
|
+
* from a React `useEffect` on first mount races the WS handshake —
|
|
37
|
+
* `socket.send()` on a non-OPEN socket throws a synchronous
|
|
38
|
+
* DOMException ("object not, or is no longer, usable"), which the
|
|
39
|
+
* caller sees as an opaque rejection and the underlying request is
|
|
40
|
+
* never sent.
|
|
41
|
+
*
|
|
42
|
+
* Each entry carries the request id so we can reject the matching
|
|
43
|
+
* pending Promise if the WS closes before we ever flush.
|
|
44
|
+
*/
|
|
45
|
+
private sendQueue;
|
|
33
46
|
/**
|
|
34
47
|
* Initializes the WebSocket connection immediately.
|
|
35
48
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HubWebSocket.d.ts","sourceRoot":"","sources":["../../src/hub/HubWebSocket.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA+DpE;;GAEG;AACH,qBAAa,YAAa,SAAQ,OAAO;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;IACH,OAAO,CAAC,eAAe,CAAoC;IAE3D;;OAEG;;
|
|
1
|
+
{"version":3,"file":"HubWebSocket.d.ts","sourceRoot":"","sources":["../../src/hub/HubWebSocket.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA+DpE;;GAEG;AACH,qBAAa,YAAa,SAAQ,OAAO;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;IACH,OAAO,CAAC,eAAe,CAAoC;IAE3D;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS,CAA2C;IAE5D;;OAEG;;IAqHH;;;;;OAKG;IACH,MAAM,GAAI,OAAO,MAAM,EAAE,aAAa,WAAW,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,cAAc,CAAC,CA6C5F;IAED;;;OAGG;IACH,wBAAwB,GAAI,KAAK,cAAc,UAO9C;IAED,UAAU,QAAO,IAAI,CAEpB;IAED,SAAS,CAAC,IAAI,GAAI,WAAW,MAAM,EAAE,UAAU,MAAM,KAAG,IAAI,CAE3D;CACJ"}
|
package/dist/hub/HubWebSocket.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HubBase}from"./HubBase";import{MessageType}from"./CommandMessage";import{getDebugPanel}from"./DebugPanel";function b64toBlob(e,t="application/octet-stream",s=512){const n=atob(e),o=[];for(let e=0;e<n.length;e+=s){const t=n.slice(e,e+s),a=new Array(t.length);for(let e=0;e<t.length;e++)a[e]=t.charCodeAt(e);const i=new Uint8Array(a);o.push(i.buffer)}return new Blob(o,{type:t})}function downloadBlob(e,t){const s=window.URL.createObjectURL(e),n=document.createElement("a");n.href=s,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(s)}export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise((n,o)=>{const a=++this.requestId;this.pendingRequests.set(a,{resolve:n,reject:o});const i={transaction_id:a,topic:e,data:s||{},timecode:Date.now(),message_type:t,success:!1,error_message:""};getDebugPanel().messageSent(MessageType[t],e,a)
|
|
1
|
+
import{HubBase}from"./HubBase";import{MessageType}from"./CommandMessage";import{getDebugPanel}from"./DebugPanel";function b64toBlob(e,t="application/octet-stream",s=512){const n=atob(e),o=[];for(let e=0;e<n.length;e+=s){const t=n.slice(e,e+s),a=new Array(t.length);for(let e=0;e<t.length;e++)a[e]=t.charCodeAt(e);const i=new Uint8Array(a);o.push(i.buffer)}return new Blob(o,{type:t})}function downloadBlob(e,t){const s=window.URL.createObjectURL(e),n=document.createElement("a");n.href=s,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(s)}export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"sendQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise((n,o)=>{const a=++this.requestId;this.pendingRequests.set(a,{resolve:n,reject:o});const i={transaction_id:a,topic:e,data:s||{},timecode:Date.now(),message_type:t,success:!1,error_message:""};getDebugPanel().messageSent(MessageType[t],e,a);const r=JSON.stringify(i);switch(this.socket.readyState){case WebSocket.OPEN:this.socket.send(r);break;case WebSocket.CONNECTING:this.sendQueue.push({id:a,json:r});break;default:this.pendingRequests.delete(a),o(new Error(`WebSocket not open (readyState=${this.socket.readyState}); cannot send '${e}'`))}})}),Object.defineProperty(this,"handleUnsolicitedMessage",{enumerable:!0,configurable:!0,writable:!0,value:e=>{e.message_type==MessageType.Broadcast&&e.topic.length>1&&this.publish(e.topic,e.data)}}),Object.defineProperty(this,"disconnect",{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.socket.close()}}),Object.defineProperty(this,"emit",{enumerable:!0,configurable:!0,writable:!0,value:(e,t)=>{this.socket.send(JSON.stringify({eventName:e,payload:t}))}}),getDebugPanel().hubCreated();const e=window.location.hostname,t=window.location.port,s=`${"https:"===window.location.protocol?"wss://":"ws://"}${e}${t?":"+t:""}/ws/`;this.socket=new WebSocket(s);let n=this;this.socket.onopen=function(){if(getDebugPanel().wsOpened(s),n.sendQueue.length>0){for(const{json:e}of n.sendQueue)n.socket.send(e);n.sendQueue=[]}n.publish("HUB/connected",!0),n.setIsConnected(!0)},this.socket.onmessage=e=>{const t=JSON.parse(e.data);if(getDebugPanel().messageReceived(MessageType[t.message_type]||String(t.message_type),t.topic,t.transaction_id),t.transaction_id&&this.pendingRequests.has(t.transaction_id)){const{resolve:e,reject:s}=this.pendingRequests.get(t.transaction_id);if("FILE_DOWNLOAD"===t.topic&&void 0!==t.data.file_name&&null!==t.data.file_name&&t.data.file_name.length>0){let n=t.data.file_name.split("/");const o=n[n.length-1];if(t.success){downloadBlob(b64toBlob(t.data),o),t.data=o,e(t)}else s(new Error(t.error_message))}else t.success?e(t):s(new Error(t.error_message));this.pendingRequests.delete(t.transaction_id)}else this.handleUnsolicitedMessage(t)},this.socket.onerror=e=>{getDebugPanel().wsError(String(e))},this.socket.onclose=e=>{if(n.setIsConnected(!1),getDebugPanel().wsClosed(e.code,e.reason||"No reason"),n.sendQueue.length>0){for(const{id:e}of n.sendQueue){const t=n.pendingRequests.get(e);t&&(t.reject(new Error("WebSocket closed before message could be sent")),n.pendingRequests.delete(e))}n.sendQueue=[]}}}}
|
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 {
|