@adcops/autocore-react 3.3.85 → 3.3.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/assets/AxisC.d.ts +4 -0
  2. package/dist/assets/AxisC.d.ts.map +1 -0
  3. package/dist/assets/AxisC.js +1 -0
  4. package/dist/assets/AxisX.js +1 -1
  5. package/dist/assets/AxisY.js +1 -1
  6. package/dist/assets/AxisZ.js +1 -1
  7. package/dist/components/ValueInput.css +9 -12
  8. package/dist/components/ValueInput.d.ts +45 -154
  9. package/dist/components/ValueInput.d.ts.map +1 -1
  10. package/dist/components/ValueInput.js +1 -1
  11. package/dist/components/ams/AmsProvider.d.ts +10 -0
  12. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  13. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  14. package/dist/components/ams/AssetRegistryTable.js +1 -1
  15. package/dist/components/forms/FormRow.d.ts +20 -0
  16. package/dist/components/forms/FormRow.d.ts.map +1 -0
  17. package/dist/components/forms/FormRow.js +1 -0
  18. package/dist/components/forms/FormSection.d.ts +19 -0
  19. package/dist/components/forms/FormSection.d.ts.map +1 -0
  20. package/dist/components/forms/FormSection.js +1 -0
  21. package/dist/components/forms/forms.css +89 -0
  22. package/dist/components/forms/index.d.ts +3 -0
  23. package/dist/components/forms/index.d.ts.map +1 -0
  24. package/dist/components/forms/index.js +1 -0
  25. package/dist/components/tis-editor/TisConfigEditor.css +121 -0
  26. package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
  27. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
  28. package/dist/components/tis-editor/TisConfigEditor.js +1 -0
  29. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
  30. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
  31. package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
  32. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
  33. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
  34. package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
  35. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
  36. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
  37. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
  38. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
  39. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
  40. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
  41. package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
  42. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
  43. package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
  44. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
  45. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
  46. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
  47. package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
  48. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
  49. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
  50. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
  51. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
  52. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
  53. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
  54. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
  55. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
  56. package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
  57. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
  58. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
  59. package/dist/components/tis-editor/types.d.ts +78 -0
  60. package/dist/components/tis-editor/types.d.ts.map +1 -0
  61. package/dist/components/tis-editor/types.js +1 -0
  62. package/dist/components/tis-editor/validation.d.ts +20 -0
  63. package/dist/components/tis-editor/validation.d.ts.map +1 -0
  64. package/dist/components/tis-editor/validation.js +1 -0
  65. package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
  66. package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
  67. package/dist/hooks/useAmsAssetTypes.js +1 -0
  68. package/dist/hooks/useTisConfig.d.ts +51 -0
  69. package/dist/hooks/useTisConfig.d.ts.map +1 -0
  70. package/dist/hooks/useTisConfig.js +1 -0
  71. package/package.json +9 -3
  72. package/src/assets/AxisC.tsx +38 -0
  73. package/src/assets/AxisX.tsx +32 -32
  74. package/src/assets/AxisY.tsx +34 -34
  75. package/src/assets/AxisZ.tsx +31 -31
  76. package/src/components/ValueInput.css +9 -12
  77. package/src/components/ValueInput.tsx +132 -317
  78. package/src/components/ams/AmsProvider.tsx +10 -0
  79. package/src/components/ams/AssetRegistryTable.tsx +53 -8
  80. package/src/components/forms/FormRow.tsx +37 -0
  81. package/src/components/forms/FormSection.tsx +39 -0
  82. package/src/components/forms/forms.css +89 -0
  83. package/src/components/forms/index.ts +2 -0
  84. package/src/components/tis-editor/TisConfigEditor.css +121 -0
  85. package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
  86. package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
  87. package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
  88. package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
  89. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
  90. package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
  91. package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
  92. package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
  93. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
  94. package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
  95. package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
  96. package/src/components/tis-editor/types.ts +95 -0
  97. package/src/components/tis-editor/validation.ts +104 -0
  98. package/src/hooks/useAmsAssetTypes.ts +70 -0
  99. package/src/hooks/useTisConfig.ts +164 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * FormRow — grid-aligned label/input pair with optional required marker
3
+ * and inline error text. Drop inside a FormSection.
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import './forms.css';
8
+
9
+ export interface FormRowProps {
10
+ label: React.ReactNode;
11
+ required?: boolean;
12
+ /** Optional descriptive hint shown beneath the label. */
13
+ hint?: React.ReactNode;
14
+ /** Inline error text shown beneath the input. */
15
+ error?: React.ReactNode;
16
+ /** Optional `htmlFor` association on the label tag. */
17
+ htmlFor?: string;
18
+ children?: React.ReactNode;
19
+ }
20
+
21
+ export const FormRow: React.FC<FormRowProps> = ({ label, required, hint, error, htmlFor, children }) => {
22
+ return (
23
+ <div className={`ac-formrow${error ? ' ac-formrow--error' : ''}`}>
24
+ <label className="ac-formrow__label" htmlFor={htmlFor}>
25
+ {label}
26
+ {required && <span className="ac-formrow__required" aria-hidden> *</span>}
27
+ {hint && <small className="ac-formrow__hint">{hint}</small>}
28
+ </label>
29
+ <div className="ac-formrow__field">
30
+ {children}
31
+ {error && <small className="ac-formrow__error">{error}</small>}
32
+ </div>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export default FormRow;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * FormSection — labeled bordered region containing a group of FormRow rows.
3
+ *
4
+ * Used by the TIS editor subforms and intended for app-level settings
5
+ * pages. Replaces the ad-hoc `.ac-form-grid` divs scattered across project
6
+ * settings views.
7
+ */
8
+
9
+ import * as React from 'react';
10
+ import './forms.css';
11
+
12
+ export interface FormSectionProps {
13
+ title?: React.ReactNode;
14
+ description?: React.ReactNode;
15
+ children?: React.ReactNode;
16
+ /** Right-aligned controls (buttons, dirty pill, etc.). */
17
+ actions?: React.ReactNode;
18
+ }
19
+
20
+ export const FormSection: React.FC<FormSectionProps> = ({ title, description, actions, children }) => {
21
+ return (
22
+ <section className="ac-formsection">
23
+ {(title || actions) && (
24
+ <header className="ac-formsection__header">
25
+ <div>
26
+ {title && <h3 className="ac-formsection__title">{title}</h3>}
27
+ {description && <small className="ac-formsection__desc">{description}</small>}
28
+ </div>
29
+ {actions && <div className="ac-formsection__actions">{actions}</div>}
30
+ </header>
31
+ )}
32
+ <div className="ac-formsection__body">
33
+ {children}
34
+ </div>
35
+ </section>
36
+ );
37
+ };
38
+
39
+ export default FormSection;
@@ -0,0 +1,89 @@
1
+ .ac-formsection {
2
+ border: 1px solid var(--surface-d, #e2e8f0);
3
+ border-radius: 6px;
4
+ background: var(--surface-card, #fff);
5
+ margin-bottom: 1rem;
6
+ }
7
+
8
+ .ac-formsection__header {
9
+ display: flex;
10
+ align-items: flex-start;
11
+ justify-content: space-between;
12
+ padding: 0.5rem 1rem;
13
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
14
+ background: var(--surface-b, #f8fafc);
15
+ border-top-left-radius: 6px;
16
+ border-top-right-radius: 6px;
17
+ }
18
+
19
+ .ac-formsection__title {
20
+ margin: 0;
21
+ font-size: 0.95rem;
22
+ font-weight: 600;
23
+ }
24
+
25
+ .ac-formsection__desc {
26
+ color: var(--text-color-secondary, #64748b);
27
+ display: block;
28
+ margin-top: 0.15rem;
29
+ }
30
+
31
+ .ac-formsection__actions {
32
+ display: flex;
33
+ gap: 0.5rem;
34
+ }
35
+
36
+ .ac-formsection__body {
37
+ padding: 0.75rem 1rem;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 0.5rem;
41
+ }
42
+
43
+ .ac-formrow {
44
+ display: grid;
45
+ grid-template-columns: minmax(8rem, 14rem) 1fr;
46
+ gap: 0.5rem 1rem;
47
+ align-items: start;
48
+ }
49
+
50
+ .ac-formrow--error .ac-formrow__field input,
51
+ .ac-formrow--error .ac-formrow__field .p-inputtext {
52
+ border-color: #dc2626;
53
+ }
54
+
55
+ .ac-formrow__label {
56
+ font-weight: 500;
57
+ padding-top: 0.4rem;
58
+ display: flex;
59
+ flex-direction: column;
60
+ }
61
+
62
+ .ac-formrow__required {
63
+ color: #dc2626;
64
+ }
65
+
66
+ .ac-formrow__hint {
67
+ color: var(--text-color-secondary, #64748b);
68
+ font-weight: 400;
69
+ font-size: 0.75rem;
70
+ margin-top: 0.15rem;
71
+ }
72
+
73
+ .ac-formrow__field {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 0.25rem;
77
+ }
78
+
79
+ .ac-formrow__field > input,
80
+ .ac-formrow__field > .p-inputtext,
81
+ .ac-formrow__field > .p-dropdown,
82
+ .ac-formrow__field > .p-inputtextarea {
83
+ width: 100%;
84
+ }
85
+
86
+ .ac-formrow__error {
87
+ color: #dc2626;
88
+ font-size: 0.75rem;
89
+ }
@@ -0,0 +1,2 @@
1
+ export { FormSection, type FormSectionProps } from './FormSection';
2
+ export { FormRow, type FormRowProps } from './FormRow';
@@ -0,0 +1,121 @@
1
+ .tis-editor {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ }
7
+
8
+ .tis-editor__header {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ padding: 0.75rem 1rem;
13
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
14
+ }
15
+
16
+ .tis-editor__header h2 {
17
+ margin: 0;
18
+ font-size: 1.125rem;
19
+ }
20
+
21
+ .tis-editor__header-actions {
22
+ display: flex;
23
+ gap: 0.5rem;
24
+ }
25
+
26
+ .tis-editor__dirty-pill {
27
+ display: inline-block;
28
+ background: #ea580c;
29
+ color: white;
30
+ font-size: 0.7rem;
31
+ font-weight: 600;
32
+ text-transform: uppercase;
33
+ letter-spacing: 0.05em;
34
+ padding: 0.15rem 0.5rem;
35
+ border-radius: 999px;
36
+ margin-left: 0.5rem;
37
+ vertical-align: middle;
38
+ }
39
+
40
+ .tis-editor__body {
41
+ flex: 1;
42
+ display: flex;
43
+ min-height: 0;
44
+ }
45
+
46
+ .tis-editor__sidebar {
47
+ flex: 0 0 320px;
48
+ border-right: 1px solid var(--surface-d, #e2e8f0);
49
+ display: flex;
50
+ flex-direction: column;
51
+ min-height: 0;
52
+ }
53
+
54
+ .tis-editor__sidebar-actions {
55
+ display: flex;
56
+ gap: 0.25rem;
57
+ padding: 0.5rem;
58
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
59
+ flex-wrap: wrap;
60
+ }
61
+
62
+ .tis-editor__sidebar-actions .p-button {
63
+ flex: 1 1 auto;
64
+ min-width: 0;
65
+ }
66
+
67
+ .tis-editor__detail {
68
+ flex: 1;
69
+ display: flex;
70
+ flex-direction: column;
71
+ min-height: 0;
72
+ }
73
+
74
+ .tis-editor__detail-header {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ padding: 0.5rem 1rem;
79
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
80
+ background: var(--surface-b, #f8fafc);
81
+ }
82
+
83
+ .tis-editor__editor-wrap {
84
+ flex: 1;
85
+ min-height: 0;
86
+ }
87
+
88
+ .tis-editor__editor-wrap > * {
89
+ height: 100%;
90
+ }
91
+
92
+ .tis-editor__error {
93
+ background: #fef2f2;
94
+ color: #991b1b;
95
+ border-left: 3px solid #dc2626;
96
+ padding: 0.5rem 1rem;
97
+ margin: 0.5rem 1rem;
98
+ font-size: 0.875rem;
99
+ }
100
+
101
+ .tis-editor__error pre {
102
+ white-space: pre-wrap;
103
+ margin: 0.25rem 0 0 0;
104
+ font-family: ui-monospace, monospace;
105
+ font-size: 0.8rem;
106
+ }
107
+
108
+ .tis-editor__empty {
109
+ flex: 1;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ color: var(--text-color-secondary, #64748b);
114
+ }
115
+
116
+ .tis-editor__new-method-label {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.25rem;
120
+ margin-bottom: 0.25rem;
121
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * TisConfigEditor — master-detail UI for editing TIS test_methods.
3
+ *
4
+ * Left: PrimeReact DataTable listing methods (method_id + label).
5
+ * Right: tabbed editor for the selected method. Phase 1 ships a single
6
+ * JSON-via-Monaco tab; Phase 2 layers form editors on top of it.
7
+ * Action bar: New / Duplicate / Delete / Apply / Save / Revert.
8
+ *
9
+ * "Apply" pushes the local Monaco buffer to the server-side stage
10
+ * (`tis.put_method`). "Save" persists the entire stage to project.json
11
+ * (`tis.save_config`). "Revert" drops the stage (`tis.discard_config_changes`).
12
+ *
13
+ * Save is disabled (with a tooltip) when active tests are open against
14
+ * any method — the server enforces the same gate, but disabling on the
15
+ * client gives clearer UX.
16
+ */
17
+
18
+ import { useEffect, useMemo, useState } from 'react';
19
+ import { DataTable } from 'primereact/datatable';
20
+ import { Column } from 'primereact/column';
21
+ import { Button } from 'primereact/button';
22
+ import { InputText } from 'primereact/inputtext';
23
+ import { Dialog } from 'primereact/dialog';
24
+ import { useContext } from 'react';
25
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
26
+ import { MessageType } from '../../hub/CommandMessage';
27
+ import { useTisConfig, type TisIpcInvoker } from '../../hooks/useTisConfig';
28
+ import { useAmsAssetTypes } from '../../hooks/useAmsAssetTypes';
29
+ import { MethodFormEditor } from './editor/MethodFormEditor';
30
+ import { SaveDiffDialog } from './editor/SaveDiffDialog';
31
+ import type { TestMethod } from './types';
32
+
33
+ import './TisConfigEditor.css';
34
+
35
+ export interface TisConfigEditorProps {
36
+ /** Project ID. Today this is informational (the server hosts one
37
+ * project) but the wire contract carries it for forward compatibility. */
38
+ projectId: string;
39
+ /** Optional invoker override — primarily for the playground / tests. */
40
+ invoker?: TisIpcInvoker;
41
+ }
42
+
43
+ interface MethodRow {
44
+ id: string;
45
+ label: string;
46
+ }
47
+
48
+ const EMPTY_METHOD: TestMethod = {
49
+ label: '',
50
+ description: '',
51
+ project_fields: [],
52
+ config_fields: [],
53
+ cycle_fields: [],
54
+ results_fields: [],
55
+ views: {},
56
+ asset_refs: [],
57
+ };
58
+
59
+ export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, invoker }) => {
60
+ const ctx = useContext(EventEmitterContext);
61
+ // Resolve invoker once so it can be passed into the SaveDiffDialog
62
+ // alongside the hook's internal use of it.
63
+ const effectiveInvoker: TisIpcInvoker = invoker
64
+ ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
65
+
66
+ const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
67
+ const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
68
+ const [selectedId, setSelectedId] = useState<string | null>(null);
69
+ const [draftError, setDraftError] = useState<string | null>(null);
70
+ const [busy, setBusy] = useState<boolean>(false);
71
+ const [saveDialogOpen, setSaveDialogOpen] = useState<boolean>(false);
72
+
73
+ // New-method dialog state.
74
+ const [newDialogOpen, setNewDialogOpen] = useState<boolean>(false);
75
+ const [newId, setNewId] = useState<string>('');
76
+
77
+ const rows: MethodRow[] = useMemo(() => {
78
+ if (!tis.config) return [];
79
+ return Object.entries(tis.config.methods).map(([id, m]) => ({
80
+ id,
81
+ label: (m as any)?.label ?? '',
82
+ }));
83
+ }, [tis.config]);
84
+
85
+ // Auto-select the default method on first successful load.
86
+ useEffect(() => {
87
+ if (!selectedId && tis.config) {
88
+ const fallback = tis.config.defaultMethodId
89
+ || Object.keys(tis.config.methods)[0]
90
+ || null;
91
+ setSelectedId(fallback);
92
+ }
93
+ }, [tis.config, selectedId]);
94
+
95
+ // Clear errors whenever the selected method changes.
96
+ useEffect(() => { setDraftError(null); }, [selectedId]);
97
+
98
+ const onApply = async (next: TestMethod) => {
99
+ if (!selectedId) return;
100
+ setBusy(true);
101
+ try {
102
+ await tis.putMethod(selectedId, next as any);
103
+ setDraftError(null);
104
+ } catch (e: any) {
105
+ setDraftError(String(e?.message ?? e));
106
+ } finally {
107
+ setBusy(false);
108
+ }
109
+ };
110
+
111
+ const onCreate = async () => {
112
+ const id = newId.trim();
113
+ if (!id) return;
114
+ if (tis.config?.methods[id]) {
115
+ setDraftError(`A method named "${id}" already exists.`);
116
+ return;
117
+ }
118
+ setBusy(true);
119
+ try {
120
+ await tis.putMethod(id, EMPTY_METHOD);
121
+ setSelectedId(id);
122
+ setNewDialogOpen(false);
123
+ setNewId('');
124
+ } catch (e: any) {
125
+ setDraftError(String(e?.message ?? e));
126
+ } finally {
127
+ setBusy(false);
128
+ }
129
+ };
130
+
131
+ const onDuplicate = async () => {
132
+ if (!selectedId || !tis.config) return;
133
+ const source = tis.config.methods[selectedId];
134
+ if (!source) return;
135
+ let candidate = `${selectedId}_copy`;
136
+ let n = 2;
137
+ while (tis.config.methods[candidate]) {
138
+ candidate = `${selectedId}_copy_${n++}`;
139
+ }
140
+ setBusy(true);
141
+ try {
142
+ await tis.putMethod(candidate, JSON.parse(JSON.stringify(source)));
143
+ setSelectedId(candidate);
144
+ } catch (e: any) {
145
+ setDraftError(String(e?.message ?? e));
146
+ } finally {
147
+ setBusy(false);
148
+ }
149
+ };
150
+
151
+ const onDelete = async () => {
152
+ if (!selectedId) return;
153
+ if (!window.confirm(`Remove method "${selectedId}"? This is staged — Save persists it.`)) return;
154
+ setBusy(true);
155
+ try {
156
+ await tis.removeMethod(selectedId);
157
+ setSelectedId(null);
158
+ } catch (e: any) {
159
+ setDraftError(String(e?.message ?? e));
160
+ } finally {
161
+ setBusy(false);
162
+ }
163
+ };
164
+
165
+ // Save flows through the diff dialog: it fetches the current disk
166
+ // state via tis.list_schemas, shows the operator what's about to land,
167
+ // and only invokes save_config on confirm.
168
+ const openSaveDialog = () => setSaveDialogOpen(true);
169
+ const onSaveConfirm = async () => {
170
+ setBusy(true);
171
+ try {
172
+ await tis.save();
173
+ setSaveDialogOpen(false);
174
+ } catch (e: any) {
175
+ setDraftError(String(e?.message ?? e));
176
+ } finally {
177
+ setBusy(false);
178
+ }
179
+ };
180
+
181
+ const onRevert = async () => {
182
+ if (!window.confirm('Discard all in-progress edits? This cannot be undone.')) return;
183
+ setBusy(true);
184
+ try {
185
+ await tis.revert();
186
+ } catch (e: any) {
187
+ setDraftError(String(e?.message ?? e));
188
+ } finally {
189
+ setBusy(false);
190
+ }
191
+ };
192
+
193
+ return (
194
+ <div className="tis-editor">
195
+ <header className="tis-editor__header">
196
+ <h2>
197
+ Test Methods{' '}
198
+ {tis.config?.dirty && (
199
+ <span className="tis-editor__dirty-pill">unsaved</span>
200
+ )}
201
+ </h2>
202
+ <div className="tis-editor__header-actions">
203
+ <Button
204
+ label="Save…"
205
+ icon="pi pi-save"
206
+ disabled={busy || !tis.config?.dirty}
207
+ onClick={openSaveDialog}
208
+ />
209
+ <Button
210
+ label="Revert"
211
+ icon="pi pi-undo"
212
+ className="p-button-secondary"
213
+ disabled={busy || !tis.config?.dirty}
214
+ onClick={onRevert}
215
+ />
216
+ </div>
217
+ </header>
218
+
219
+ {tis.error && (
220
+ <div className="tis-editor__error">
221
+ <strong>Error:</strong> <pre>{tis.error}</pre>
222
+ </div>
223
+ )}
224
+
225
+ <div className="tis-editor__body">
226
+ <aside className="tis-editor__sidebar">
227
+ <div className="tis-editor__sidebar-actions">
228
+ <Button
229
+ label="New"
230
+ icon="pi pi-plus"
231
+ disabled={busy}
232
+ onClick={() => setNewDialogOpen(true)}
233
+ />
234
+ <Button
235
+ label="Duplicate"
236
+ icon="pi pi-clone"
237
+ className="p-button-secondary"
238
+ disabled={busy || !selectedId}
239
+ onClick={onDuplicate}
240
+ />
241
+ <Button
242
+ label="Delete"
243
+ icon="pi pi-trash"
244
+ className="p-button-danger"
245
+ disabled={busy || !selectedId}
246
+ onClick={onDelete}
247
+ />
248
+ </div>
249
+ <DataTable
250
+ value={rows}
251
+ selection={rows.find(r => r.id === selectedId) ?? null}
252
+ onSelectionChange={(e) => setSelectedId((e.value as MethodRow | null)?.id ?? null)}
253
+ selectionMode="single"
254
+ dataKey="id"
255
+ scrollable
256
+ scrollHeight="flex"
257
+ emptyMessage={tis.loading ? 'Loading…' : 'No test methods defined.'}
258
+ >
259
+ <Column field="id" header="Method ID" />
260
+ <Column field="label" header="Label" />
261
+ </DataTable>
262
+ </aside>
263
+
264
+ <section className="tis-editor__detail">
265
+ {selectedId && tis.config?.methods[selectedId] ? (
266
+ <>
267
+ {draftError && (
268
+ <div className="tis-editor__error">
269
+ <pre>{draftError}</pre>
270
+ </div>
271
+ )}
272
+ <MethodFormEditor
273
+ methodId={selectedId}
274
+ method={tis.config.methods[selectedId] as TestMethod}
275
+ onApply={onApply}
276
+ busy={busy}
277
+ knownAssetTypes={ams.types}
278
+ />
279
+ </>
280
+ ) : (
281
+ <div className="tis-editor__empty">
282
+ Select a test method on the left, or create a new one.
283
+ </div>
284
+ )}
285
+ </section>
286
+ </div>
287
+
288
+ <Dialog
289
+ header="New Test Method"
290
+ visible={newDialogOpen}
291
+ onHide={() => setNewDialogOpen(false)}
292
+ style={{ width: '24rem' }}
293
+ >
294
+ <label className="tis-editor__new-method-label">
295
+ Method ID
296
+ <InputText
297
+ value={newId}
298
+ onChange={(e) => setNewId(e.target.value)}
299
+ placeholder="e.g. translational_traction"
300
+ autoFocus
301
+ />
302
+ </label>
303
+ <small>Canonical key — appears in wire payloads, on-disk paths, and generated code.</small>
304
+ <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
305
+ <Button label="Cancel" className="p-button-text" onClick={() => setNewDialogOpen(false)} />
306
+ <Button label="Create" disabled={!newId.trim() || busy} onClick={onCreate} />
307
+ </div>
308
+ </Dialog>
309
+
310
+ <SaveDiffDialog
311
+ visible={saveDialogOpen}
312
+ staged={(tis.config?.methods ?? {}) as Record<string, TestMethod>}
313
+ invoker={effectiveInvoker}
314
+ onConfirm={onSaveConfirm}
315
+ onCancel={() => setSaveDialogOpen(false)}
316
+ />
317
+ </div>
318
+ );
319
+ };
320
+
321
+ export default TisConfigEditor;
@@ -0,0 +1,54 @@
1
+ import { InputText } from 'primereact/inputtext';
2
+ import { Checkbox } from 'primereact/checkbox';
3
+ import { FormSection } from '../../forms/FormSection';
4
+ import { FormRow } from '../../forms/FormRow';
5
+ import type { AnalysisShape, TestMethod } from '../types';
6
+
7
+ export interface AnalysisEditorProps {
8
+ method: TestMethod;
9
+ onChange: (next: TestMethod) => void;
10
+ }
11
+
12
+ const empty = (): AnalysisShape => ({ script: '', function: '' });
13
+
14
+ export const AnalysisEditor: React.FC<AnalysisEditorProps> = ({ method, onChange }) => {
15
+ const a = method.analysis as AnalysisShape | null | undefined;
16
+ const enabled = !!a;
17
+
18
+ const setAnalysis = (next: AnalysisShape | null) => {
19
+ onChange({ ...method, analysis: next });
20
+ };
21
+
22
+ return (
23
+ <FormSection
24
+ title="Analysis Hook"
25
+ description="Optional post-cycle Python entry point. Codegen wires this into the method's TestManager."
26
+ actions={
27
+ <Checkbox
28
+ checked={enabled}
29
+ onChange={(e) => setAnalysis(e.checked ? empty() : null)}
30
+ />
31
+ }
32
+ >
33
+ {!enabled && <small>Analysis hook disabled. Tick the box to enable.</small>}
34
+ {enabled && a && (
35
+ <>
36
+ <FormRow label="Script" required hint="Path under autocore-python's scripts directory.">
37
+ <InputText
38
+ value={a.script}
39
+ onChange={(e) => setAnalysis({ ...a, script: e.target.value })}
40
+ placeholder="e.g. analysis/traction_v1.py"
41
+ />
42
+ </FormRow>
43
+ <FormRow label="Function" required hint="Entry-point name within the script.">
44
+ <InputText
45
+ value={a.function}
46
+ onChange={(e) => setAnalysis({ ...a, function: e.target.value })}
47
+ placeholder="e.g. analyze_cycle"
48
+ />
49
+ </FormRow>
50
+ </>
51
+ )}
52
+ </FormSection>
53
+ );
54
+ };