@adcops/autocore-react 3.3.112 → 3.3.115

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 (52) hide show
  1. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  2. package/dist/components/ams/AmsProvider.js +1 -1
  3. package/dist/components/ams-editor/AssetFieldArray.d.ts +21 -0
  4. package/dist/components/ams-editor/AssetFieldArray.d.ts.map +1 -0
  5. package/dist/components/ams-editor/AssetFieldArray.js +1 -0
  6. package/dist/components/ams-editor/AssetTypeEditor.css +122 -0
  7. package/dist/components/ams-editor/AssetTypeEditor.d.ts +26 -0
  8. package/dist/components/ams-editor/AssetTypeEditor.d.ts.map +1 -0
  9. package/dist/components/ams-editor/AssetTypeEditor.js +1 -0
  10. package/dist/components/ams-editor/AssetTypeFormEditor.d.ts +21 -0
  11. package/dist/components/ams-editor/AssetTypeFormEditor.d.ts.map +1 -0
  12. package/dist/components/ams-editor/AssetTypeFormEditor.js +1 -0
  13. package/dist/components/ams-editor/SubLocationsEditor.d.ts +17 -0
  14. package/dist/components/ams-editor/SubLocationsEditor.d.ts.map +1 -0
  15. package/dist/components/ams-editor/SubLocationsEditor.js +1 -0
  16. package/dist/components/ams-editor/index.d.ts +7 -0
  17. package/dist/components/ams-editor/index.d.ts.map +1 -0
  18. package/dist/components/ams-editor/index.js +1 -0
  19. package/dist/components/ams-editor/types.d.ts +34 -0
  20. package/dist/components/ams-editor/types.d.ts.map +1 -0
  21. package/dist/components/ams-editor/types.js +1 -0
  22. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  23. package/dist/components/tis/ResultHistoryTable.js +1 -1
  24. package/dist/components/tis/ScienceTable.d.ts +15 -0
  25. package/dist/components/tis/ScienceTable.d.ts.map +1 -1
  26. package/dist/components/tis/ScienceTable.js +1 -1
  27. package/dist/components/tis/TestDataView.d.ts +10 -4
  28. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  29. package/dist/components/tis/TestDataView.js +1 -1
  30. package/dist/components/tis-editor/TisConfigEditor.d.ts +1 -1
  31. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -1
  32. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -1
  33. package/dist/hooks/useAmsSchemaConfig.d.ts +39 -0
  34. package/dist/hooks/useAmsSchemaConfig.d.ts.map +1 -0
  35. package/dist/hooks/useAmsSchemaConfig.js +1 -0
  36. package/dist/hooks/useTisConfig.d.ts +1 -1
  37. package/package.json +110 -110
  38. package/src/components/ams/AmsProvider.tsx +20 -15
  39. package/src/components/ams-editor/AssetFieldArray.tsx +103 -0
  40. package/src/components/ams-editor/AssetTypeEditor.css +122 -0
  41. package/src/components/ams-editor/AssetTypeEditor.tsx +295 -0
  42. package/src/components/ams-editor/AssetTypeFormEditor.tsx +178 -0
  43. package/src/components/ams-editor/SubLocationsEditor.tsx +151 -0
  44. package/src/components/ams-editor/index.ts +16 -0
  45. package/src/components/ams-editor/types.ts +40 -0
  46. package/src/components/tis/ResultHistoryTable.tsx +141 -1
  47. package/src/components/tis/ScienceTable.tsx +71 -12
  48. package/src/components/tis/TestDataView.tsx +65 -6
  49. package/src/components/tis-editor/TisConfigEditor.tsx +1 -1
  50. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +4 -3
  51. package/src/hooks/useAmsSchemaConfig.ts +142 -0
  52. package/src/hooks/useTisConfig.ts +1 -1
@@ -0,0 +1,122 @@
1
+ .ams-typeeditor {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ }
7
+
8
+ .ams-typeeditor__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
+ .ams-typeeditor__header h2 {
17
+ margin: 0;
18
+ font-size: 1.125rem;
19
+ }
20
+
21
+ .ams-typeeditor__header-actions {
22
+ display: flex;
23
+ gap: 0.5rem;
24
+ }
25
+
26
+ .ams-typeeditor__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
+ .ams-typeeditor__body {
41
+ flex: 1;
42
+ display: flex;
43
+ min-height: 0;
44
+ }
45
+
46
+ .ams-typeeditor__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
+ .ams-typeeditor__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
+ .ams-typeeditor__sidebar-actions .p-button {
63
+ flex: 1 1 auto;
64
+ min-width: 0;
65
+ }
66
+
67
+ .ams-typeeditor__builtins {
68
+ padding: 0.5rem;
69
+ border-top: 1px solid var(--surface-d, #e2e8f0);
70
+ color: var(--text-color-secondary, #64748b);
71
+ }
72
+
73
+ .ams-typeeditor__detail {
74
+ flex: 1;
75
+ display: flex;
76
+ flex-direction: column;
77
+ min-height: 0;
78
+ }
79
+
80
+ .ams-typeeditor__error {
81
+ background: #fef2f2;
82
+ color: #991b1b;
83
+ border-left: 3px solid #dc2626;
84
+ padding: 0.5rem 1rem;
85
+ margin: 0.5rem 1rem;
86
+ font-size: 0.875rem;
87
+ }
88
+
89
+ .ams-typeeditor__error pre {
90
+ white-space: pre-wrap;
91
+ margin: 0.25rem 0 0 0;
92
+ font-family: ui-monospace, monospace;
93
+ font-size: 0.8rem;
94
+ }
95
+
96
+ .ams-typeeditor__empty {
97
+ flex: 1;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ color: var(--text-color-secondary, #64748b);
102
+ }
103
+
104
+ .ams-typeeditor__new-label {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 0.25rem;
108
+ margin-bottom: 0.25rem;
109
+ }
110
+
111
+ /* Portrait HMIs: stack the panes (see TisConfigEditor.css rationale). */
112
+ @media (orientation: portrait) {
113
+ .ams-typeeditor__body {
114
+ flex-direction: column;
115
+ }
116
+
117
+ .ams-typeeditor__sidebar {
118
+ flex: 0 1 35%;
119
+ border-right: none;
120
+ border-bottom: 1px solid var(--surface-d, #e2e8f0);
121
+ }
122
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * AssetTypeEditor — master-detail UI for authoring AMS custom asset TYPES
3
+ * from the operator HMI (the Instron-style "define your own asset type"
4
+ * capability). The instance-level UI (AssetRegistryTable / AssetEditDialog)
5
+ * edits asset records; this edits the type *schemas* those records conform
6
+ * to.
7
+ *
8
+ * Left: custom types (editable). Right: tabbed AssetTypeFormEditor.
9
+ * Action bar: New / Duplicate / Delete / Save / Revert.
10
+ *
11
+ * Staged-then-saved, mirroring the TIS test-method editor: New/Apply/Delete
12
+ * stage into the AMS module's in-memory buffer (dirty); Save persists to the
13
+ * `asset_management.json` sidecar (ams.save_schema_config) and broadcasts
14
+ * ams.schema_changed so open pickers/forms refresh. Built-in types
15
+ * (load_cell/linear_encoder/spring) are code-defined and shown read-only for
16
+ * reference — they can't be edited here.
17
+ */
18
+
19
+ import { useEffect, useMemo, useState } from 'react';
20
+ import { useContext } from 'react';
21
+ import { DataTable } from 'primereact/datatable';
22
+ import { Column } from 'primereact/column';
23
+ import { Button } from 'primereact/button';
24
+ import { InputText } from 'primereact/inputtext';
25
+ import { Dialog } from 'primereact/dialog';
26
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
27
+ import { MessageType } from '../../hub/CommandMessage';
28
+ import { useAmsSchemaConfig } from '../../hooks/useAmsSchemaConfig';
29
+ import { useAmsAssetTypes } from '../../hooks/useAmsAssetTypes';
30
+ import type { TisIpcInvoker } from '../../hooks/useTisConfig';
31
+ import { AssetTypeFormEditor } from './AssetTypeFormEditor';
32
+ import type { AssetTypeDef } from './types';
33
+
34
+ import './AssetTypeEditor.css';
35
+
36
+ export interface AssetTypeEditorProps {
37
+ /** Optional invoker override — primarily for the playground / tests. */
38
+ invoker?: TisIpcInvoker;
39
+ }
40
+
41
+ interface TypeRow {
42
+ id: string;
43
+ label: string;
44
+ }
45
+
46
+ const EMPTY_TYPE: AssetTypeDef = {
47
+ label: '',
48
+ description: '',
49
+ fields: [],
50
+ calibration_fields: [],
51
+ };
52
+
53
+ export const AssetTypeEditor: React.FC<AssetTypeEditorProps> = ({ invoker }) => {
54
+ const ctx = useContext(EventEmitterContext);
55
+ // Resolve the invoker ONCE and memoize — both hooks key effects off it,
56
+ // and an invoker rebuilt every render drives a refetch loop that wipes
57
+ // in-progress edits (see useAmsSchemaConfig's ref note).
58
+ const effectiveInvoker: TisIpcInvoker = useMemo(
59
+ () => invoker
60
+ ?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any)),
61
+ [invoker, ctx],
62
+ );
63
+
64
+ const ams = useAmsSchemaConfig({ invoker: effectiveInvoker });
65
+ // Full merged catalog (built-in + custom) — used to derive the built-in
66
+ // names for the `extends` dropdown and to warn on name collisions.
67
+ const catalog = useAmsAssetTypes({ invoker: effectiveInvoker });
68
+
69
+ const [selectedId, setSelectedId] = useState<string | null>(null);
70
+ const [draftError, setDraftError] = useState<string | null>(null);
71
+ const [busy, setBusy] = useState<boolean>(false);
72
+ const [newDialogOpen, setNewDialogOpen] = useState<boolean>(false);
73
+ const [newId, setNewId] = useState<string>('');
74
+
75
+ const customIds = useMemo(
76
+ () => Object.keys(ams.config?.assetTypes ?? {}),
77
+ [ams.config],
78
+ );
79
+ const builtinTypes = useMemo(
80
+ () => catalog.types.filter(t => !customIds.includes(t)),
81
+ [catalog.types, customIds],
82
+ );
83
+
84
+ const rows: TypeRow[] = useMemo(() => {
85
+ if (!ams.config) return [];
86
+ return Object.entries(ams.config.assetTypes).map(([id, d]) => ({
87
+ id,
88
+ label: (d as AssetTypeDef)?.label ?? '',
89
+ }));
90
+ }, [ams.config]);
91
+
92
+ useEffect(() => {
93
+ if (!selectedId && ams.config) {
94
+ setSelectedId(Object.keys(ams.config.assetTypes)[0] ?? null);
95
+ }
96
+ }, [ams.config, selectedId]);
97
+
98
+ useEffect(() => { setDraftError(null); }, [selectedId]);
99
+
100
+ const onApply = async (next: AssetTypeDef) => {
101
+ if (!selectedId) return;
102
+ setBusy(true);
103
+ try {
104
+ await ams.putType(selectedId, next);
105
+ setDraftError(null);
106
+ } catch (e: any) {
107
+ setDraftError(String(e?.message ?? e));
108
+ } finally {
109
+ setBusy(false);
110
+ }
111
+ };
112
+
113
+ const onCreate = async () => {
114
+ const id = newId.trim();
115
+ if (!id) return;
116
+ if (ams.config?.assetTypes[id]) {
117
+ setDraftError(`A custom type named "${id}" already exists.`);
118
+ return;
119
+ }
120
+ if (builtinTypes.includes(id)) {
121
+ // Allowed (it becomes an override/extension of the built-in), but
122
+ // warn so the operator knows they're shadowing a built-in.
123
+ if (!window.confirm(
124
+ `"${id}" is a built-in type. Creating a custom type with this id ` +
125
+ `overrides/extends the built-in. Continue?`)) return;
126
+ }
127
+ setBusy(true);
128
+ try {
129
+ await ams.putType(id, EMPTY_TYPE);
130
+ setSelectedId(id);
131
+ setNewDialogOpen(false);
132
+ setNewId('');
133
+ } catch (e: any) {
134
+ setDraftError(String(e?.message ?? e));
135
+ } finally {
136
+ setBusy(false);
137
+ }
138
+ };
139
+
140
+ const onDuplicate = async () => {
141
+ if (!selectedId || !ams.config) return;
142
+ const source = ams.config.assetTypes[selectedId];
143
+ if (!source) return;
144
+ let candidate = `${selectedId}_copy`;
145
+ let n = 2;
146
+ while (ams.config.assetTypes[candidate]) candidate = `${selectedId}_copy_${n++}`;
147
+ setBusy(true);
148
+ try {
149
+ await ams.putType(candidate, JSON.parse(JSON.stringify(source)));
150
+ setSelectedId(candidate);
151
+ } catch (e: any) {
152
+ setDraftError(String(e?.message ?? e));
153
+ } finally {
154
+ setBusy(false);
155
+ }
156
+ };
157
+
158
+ const onDelete = async () => {
159
+ if (!selectedId) return;
160
+ if (!window.confirm(`Remove type "${selectedId}"? This is staged — Save persists it.`)) return;
161
+ setBusy(true);
162
+ try {
163
+ await ams.removeType(selectedId);
164
+ setSelectedId(null);
165
+ } catch (e: any) {
166
+ // Server refuses when assets still use the type — surface it.
167
+ setDraftError(String(e?.message ?? e));
168
+ } finally {
169
+ setBusy(false);
170
+ }
171
+ };
172
+
173
+ const onSave = async () => {
174
+ if (!window.confirm('Save asset-type changes to disk?')) return;
175
+ setBusy(true);
176
+ try {
177
+ await ams.save();
178
+ } catch (e: any) {
179
+ setDraftError(String(e?.message ?? e));
180
+ } finally {
181
+ setBusy(false);
182
+ }
183
+ };
184
+
185
+ const onRevert = async () => {
186
+ if (!window.confirm('Discard all in-progress edits? This cannot be undone.')) return;
187
+ setBusy(true);
188
+ try {
189
+ await ams.revert();
190
+ } catch (e: any) {
191
+ setDraftError(String(e?.message ?? e));
192
+ } finally {
193
+ setBusy(false);
194
+ }
195
+ };
196
+
197
+ return (
198
+ <div className="ams-typeeditor">
199
+ <header className="ams-typeeditor__header">
200
+ <h2>
201
+ Asset Types{' '}
202
+ {ams.config?.dirty && <span className="ams-typeeditor__dirty-pill">unsaved</span>}
203
+ </h2>
204
+ <div className="ams-typeeditor__header-actions">
205
+ <Button label="Save…" icon="pi pi-save"
206
+ disabled={busy || !ams.config?.dirty} onClick={onSave} />
207
+ <Button label="Revert" icon="pi pi-undo" className="p-button-secondary"
208
+ disabled={busy || !ams.config?.dirty} onClick={onRevert} />
209
+ </div>
210
+ </header>
211
+
212
+ {ams.error && (
213
+ <div className="ams-typeeditor__error">
214
+ <strong>Error:</strong> <pre>{ams.error}</pre>
215
+ </div>
216
+ )}
217
+
218
+ <div className="ams-typeeditor__body">
219
+ <aside className="ams-typeeditor__sidebar">
220
+ <div className="ams-typeeditor__sidebar-actions">
221
+ <Button label="New" icon="pi pi-plus"
222
+ disabled={busy} onClick={() => setNewDialogOpen(true)} />
223
+ <Button label="Duplicate" icon="pi pi-clone" className="p-button-secondary"
224
+ disabled={busy || !selectedId} onClick={onDuplicate} />
225
+ <Button label="Delete" icon="pi pi-trash" className="p-button-danger"
226
+ disabled={busy || !selectedId} onClick={onDelete} />
227
+ </div>
228
+ <DataTable
229
+ value={rows}
230
+ selection={rows.find(r => r.id === selectedId) ?? null}
231
+ onSelectionChange={(e) => setSelectedId((e.value as TypeRow | null)?.id ?? null)}
232
+ selectionMode="single"
233
+ dataKey="id"
234
+ scrollable
235
+ scrollHeight="flex"
236
+ emptyMessage={ams.loading ? 'Loading…' : 'No custom asset types defined.'}
237
+ >
238
+ <Column field="id" header="Type ID" />
239
+ <Column field="label" header="Label" />
240
+ </DataTable>
241
+ {builtinTypes.length > 0 && (
242
+ <div className="ams-typeeditor__builtins">
243
+ <small>Built-in (read-only): {builtinTypes.join(', ')}</small>
244
+ </div>
245
+ )}
246
+ </aside>
247
+
248
+ <section className="ams-typeeditor__detail">
249
+ {selectedId && ams.config?.assetTypes[selectedId] ? (
250
+ <>
251
+ {draftError && (
252
+ <div className="ams-typeeditor__error"><pre>{draftError}</pre></div>
253
+ )}
254
+ <AssetTypeFormEditor
255
+ typeId={selectedId}
256
+ def={ams.config.assetTypes[selectedId] as AssetTypeDef}
257
+ onApply={onApply}
258
+ busy={busy}
259
+ builtinTypes={builtinTypes}
260
+ />
261
+ </>
262
+ ) : (
263
+ <div className="ams-typeeditor__empty">
264
+ Select a custom asset type on the left, or create a new one.
265
+ </div>
266
+ )}
267
+ </section>
268
+ </div>
269
+
270
+ <Dialog
271
+ header="New Asset Type"
272
+ visible={newDialogOpen}
273
+ onHide={() => setNewDialogOpen(false)}
274
+ style={{ width: '24rem' }}
275
+ >
276
+ <label className="ams-typeeditor__new-label">
277
+ Type ID
278
+ <InputText
279
+ value={newId}
280
+ onChange={(e) => setNewId(e.target.value)}
281
+ placeholder="e.g. triaxial_transducer"
282
+ autoFocus
283
+ />
284
+ </label>
285
+ <small>Canonical key — appears in wire payloads, on-disk paths, and generated code.</small>
286
+ <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
287
+ <Button label="Cancel" className="p-button-text" onClick={() => setNewDialogOpen(false)} />
288
+ <Button label="Create" disabled={!newId.trim() || busy} onClick={onCreate} />
289
+ </div>
290
+ </Dialog>
291
+ </div>
292
+ );
293
+ };
294
+
295
+ export default AssetTypeEditor;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * AssetTypeFormEditor — tabbed shell for a single custom asset type,
3
+ * mirroring the TIS MethodFormEditor. Tabs: Identity, Fields, Calibration
4
+ * Fields, Sub-locations, and an always-available JSON escape hatch.
5
+ *
6
+ * Owns the local working copy; hands the changed copy back via onApply —
7
+ * the parent decides when to call ams.put_asset_type.
8
+ */
9
+
10
+ import { useEffect, useMemo, useState } from 'react';
11
+ import { TabView, TabPanel } from 'primereact/tabview';
12
+ import { Button } from 'primereact/button';
13
+ import { InputText } from 'primereact/inputtext';
14
+ import { InputTextarea } from 'primereact/inputtextarea';
15
+ import { Dropdown } from 'primereact/dropdown';
16
+ import { CodeEditor } from '../CodeEditor';
17
+ import { FormSection } from '../forms/FormSection';
18
+ import { FormRow } from '../forms/FormRow';
19
+ import { AssetFieldArray } from './AssetFieldArray';
20
+ import { SubLocationsEditor } from './SubLocationsEditor';
21
+ import type { AssetTypeDef, TestField } from './types';
22
+
23
+ export interface AssetTypeFormEditorProps {
24
+ typeId: string;
25
+ def: AssetTypeDef;
26
+ /** Called on Apply with the working copy — parent forwards to put_asset_type. */
27
+ onApply: (next: AssetTypeDef) => Promise<void> | void;
28
+ /** Built-in type names, for the `extends` dropdown. */
29
+ builtinTypes?: string[];
30
+ busy?: boolean;
31
+ }
32
+
33
+ export const AssetTypeFormEditor: React.FC<AssetTypeFormEditorProps> = ({
34
+ typeId, def, onApply, builtinTypes = [], busy,
35
+ }) => {
36
+ const [draft, setDraft] = useState<AssetTypeDef>(def);
37
+ const [jsonText, setJsonText] = useState<string>('');
38
+ const [jsonError, setJsonError] = useState<string | null>(null);
39
+ const [activeTab, setActiveTab] = useState<number>(0);
40
+
41
+ // Reset the working copy whenever the incoming type changes (selection
42
+ // change, or a fresh version after save). Unapplied edits are discarded.
43
+ useEffect(() => {
44
+ setDraft(def);
45
+ setJsonText(JSON.stringify(def, null, 2));
46
+ setJsonError(null);
47
+ }, [def, typeId]);
48
+
49
+ const dirty = useMemo(() => {
50
+ try { return JSON.stringify(draft) !== JSON.stringify(def); }
51
+ catch { return true; }
52
+ }, [draft, def]);
53
+
54
+ const handleFormChange = (next: AssetTypeDef) => {
55
+ setDraft(next);
56
+ setJsonText(JSON.stringify(next, null, 2));
57
+ };
58
+
59
+ const handleJsonChange = (v: string | undefined) => {
60
+ const text = v ?? '';
61
+ setJsonText(text);
62
+ try {
63
+ setDraft(JSON.parse(text));
64
+ setJsonError(null);
65
+ } catch (e: any) {
66
+ setJsonError(String(e?.message ?? e));
67
+ }
68
+ };
69
+
70
+ const handleApply = async () => {
71
+ if (jsonError) return;
72
+ await onApply(draft);
73
+ };
74
+
75
+ const extendsOptions = [
76
+ { label: '— none —', value: '' },
77
+ ...builtinTypes.map(t => ({ label: `builtin: ${t}`, value: t })),
78
+ ];
79
+
80
+ return (
81
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
82
+ <div
83
+ style={{
84
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
85
+ padding: '0.5rem 1rem',
86
+ borderBottom: '1px solid var(--surface-d, #e2e8f0)',
87
+ background: 'var(--surface-b, #f8fafc)',
88
+ }}
89
+ >
90
+ <strong>
91
+ {typeId}
92
+ {dirty && <span style={{ marginLeft: '0.5rem', color: '#ea580c' }}>•</span>}
93
+ </strong>
94
+ <Button
95
+ label="Apply"
96
+ icon="pi pi-check"
97
+ disabled={busy || !dirty || !!jsonError}
98
+ onClick={handleApply}
99
+ />
100
+ </div>
101
+
102
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
103
+ <TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
104
+ <TabPanel header="Identity">
105
+ <FormSection title="Identity" description="How this type appears in the Add-Asset picker and how its asset IDs are prefixed.">
106
+ <FormRow label="Label" hint="Pretty name in the asset picker. Falls back to the type id.">
107
+ <InputText
108
+ value={draft.label ?? ''}
109
+ onChange={(e) => handleFormChange({ ...draft, label: e.target.value || undefined })}
110
+ />
111
+ </FormRow>
112
+ <FormRow label="Description">
113
+ <InputTextarea
114
+ rows={2}
115
+ value={draft.description ?? ''}
116
+ onChange={(e) => handleFormChange({ ...draft, description: e.target.value || undefined })}
117
+ />
118
+ </FormRow>
119
+ <FormRow label="ID prefix" hint="Asset IDs are minted as <prefix>001, 002, … Defaults to A- when blank.">
120
+ <InputText
121
+ value={draft.id_prefix ?? ''}
122
+ placeholder="e.g. TSDR-"
123
+ onChange={(e) => handleFormChange({ ...draft, id_prefix: e.target.value || undefined })}
124
+ />
125
+ </FormRow>
126
+ <FormRow label="Extends" hint="Optionally build on a built-in type's schema (merges its fields under yours).">
127
+ <Dropdown
128
+ value={draft.extends ?? ''}
129
+ options={extendsOptions}
130
+ onChange={(e) => handleFormChange({ ...draft, extends: e.value || undefined })}
131
+ editable
132
+ />
133
+ </FormRow>
134
+ </FormSection>
135
+ </TabPanel>
136
+ <TabPanel header="Fields">
137
+ <AssetFieldArray
138
+ title="Nameplate Fields"
139
+ description="Per-asset fields captured when the asset is registered (e.g. capacity, material)."
140
+ fields={(draft.fields ?? []) as TestField[]}
141
+ onChange={(next) => handleFormChange({ ...draft, fields: next })}
142
+ />
143
+ </TabPanel>
144
+ <TabPanel header="Calibration Fields">
145
+ <AssetFieldArray
146
+ title="Calibration Fields"
147
+ description="The calibration-record schema (e.g. scale, offset). Leave empty for types that don't calibrate."
148
+ fields={(draft.calibration_fields ?? []) as TestField[]}
149
+ onChange={(next) => handleFormChange({ ...draft, calibration_fields: next })}
150
+ />
151
+ </TabPanel>
152
+ <TabPanel header="Sub-locations">
153
+ <SubLocationsEditor def={draft} onChange={handleFormChange} />
154
+ </TabPanel>
155
+ <TabPanel header="JSON">
156
+ {jsonError && (
157
+ <div style={{ color: '#dc2626', marginBottom: '0.5rem', fontFamily: 'monospace' }}>
158
+ {jsonError}
159
+ </div>
160
+ )}
161
+ <div style={{ height: '60vh' }}>
162
+ <CodeEditor
163
+ code={jsonText}
164
+ language="json"
165
+ theme="vs-dark"
166
+ readOnly={false}
167
+ cursorStyle="line"
168
+ onCodeChanged={handleJsonChange}
169
+ />
170
+ </div>
171
+ </TabPanel>
172
+ </TabView>
173
+ </div>
174
+ </div>
175
+ );
176
+ };
177
+
178
+ export default AssetTypeFormEditor;