@adcops/autocore-react 3.3.111 → 3.3.113
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/assets/JogYDownLeft.d.ts +4 -0
- package/dist/assets/JogYDownLeft.d.ts.map +1 -0
- package/dist/assets/JogYDownLeft.js +1 -0
- package/dist/assets/JogYUpRight.d.ts +4 -0
- package/dist/assets/JogYUpRight.d.ts.map +1 -0
- package/dist/assets/JogYUpRight.js +1 -0
- package/dist/assets/index.d.ts +2 -2
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/components/JogPanel.js +1 -1
- package/dist/components/ams/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AmsProvider.js +1 -1
- package/dist/components/ams-editor/AssetFieldArray.d.ts +21 -0
- package/dist/components/ams-editor/AssetFieldArray.d.ts.map +1 -0
- package/dist/components/ams-editor/AssetFieldArray.js +1 -0
- package/dist/components/ams-editor/AssetTypeEditor.css +122 -0
- package/dist/components/ams-editor/AssetTypeEditor.d.ts +26 -0
- package/dist/components/ams-editor/AssetTypeEditor.d.ts.map +1 -0
- package/dist/components/ams-editor/AssetTypeEditor.js +1 -0
- package/dist/components/ams-editor/AssetTypeFormEditor.d.ts +21 -0
- package/dist/components/ams-editor/AssetTypeFormEditor.d.ts.map +1 -0
- package/dist/components/ams-editor/AssetTypeFormEditor.js +1 -0
- package/dist/components/ams-editor/SubLocationsEditor.d.ts +17 -0
- package/dist/components/ams-editor/SubLocationsEditor.d.ts.map +1 -0
- package/dist/components/ams-editor/SubLocationsEditor.js +1 -0
- package/dist/components/ams-editor/index.d.ts +7 -0
- package/dist/components/ams-editor/index.d.ts.map +1 -0
- package/dist/components/ams-editor/index.js +1 -0
- package/dist/components/ams-editor/types.d.ts +34 -0
- package/dist/components/ams-editor/types.d.ts.map +1 -0
- package/dist/components/ams-editor/types.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts +1 -1
- package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -1
- package/dist/hooks/useAmsSchemaConfig.d.ts +39 -0
- package/dist/hooks/useAmsSchemaConfig.d.ts.map +1 -0
- package/dist/hooks/useAmsSchemaConfig.js +1 -0
- package/dist/hooks/useTisConfig.d.ts +1 -1
- package/package.json +1 -1
- package/src/assets/{JogYPos.tsx → JogYDownLeft.tsx} +2 -2
- package/src/assets/{JogYNeg.tsx → JogYUpRight.tsx} +3 -2
- package/src/assets/index.ts +2 -2
- package/src/components/JogPanel.tsx +7 -7
- package/src/components/ams/AmsProvider.tsx +20 -15
- package/src/components/ams-editor/AssetFieldArray.tsx +103 -0
- package/src/components/ams-editor/AssetTypeEditor.css +122 -0
- package/src/components/ams-editor/AssetTypeEditor.tsx +295 -0
- package/src/components/ams-editor/AssetTypeFormEditor.tsx +178 -0
- package/src/components/ams-editor/SubLocationsEditor.tsx +151 -0
- package/src/components/ams-editor/index.ts +16 -0
- package/src/components/ams-editor/types.ts +40 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +1 -1
- package/src/components/tis-editor/editor/SaveDiffDialog.tsx +4 -3
- package/src/hooks/useAmsSchemaConfig.ts +142 -0
- package/src/hooks/useTisConfig.ts +1 -1
- package/dist/assets/JogYNeg.d.ts +0 -4
- package/dist/assets/JogYNeg.d.ts.map +0 -1
- package/dist/assets/JogYNeg.js +0 -1
- package/dist/assets/JogYPos.d.ts +0 -4
- package/dist/assets/JogYPos.d.ts.map +0 -1
- package/dist/assets/JogYPos.js +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { SVGProps } from "react";
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const SvgJogYUpRight = (props: SVGProps<SVGSVGElement>) => (
|
|
11
11
|
<svg
|
|
12
12
|
xmlns="http://www.w3.org/2000/svg"
|
|
13
13
|
width="24"
|
|
@@ -27,4 +27,5 @@ export const SvgJogYPos = (props: SVGProps<SVGSVGElement>) => (
|
|
|
27
27
|
</svg>
|
|
28
28
|
);
|
|
29
29
|
|
|
30
|
-
export default
|
|
30
|
+
export default SvgJogYUpRight;
|
|
31
|
+
|
package/src/assets/index.ts
CHANGED
|
@@ -5,8 +5,8 @@ export { default as JogMedium } from "./JogMedium";
|
|
|
5
5
|
export { default as JogShort } from "./JogShort";
|
|
6
6
|
export { default as JogXRight } from "./JogXRight";
|
|
7
7
|
export { default as JogXLeft } from "./JogXLeft";
|
|
8
|
-
export { default as
|
|
9
|
-
export { default as
|
|
8
|
+
export { default as JogYUpRight } from "./JogYUpRight";
|
|
9
|
+
export { default as JogYDownLeft } from "./JogYDownLeft";
|
|
10
10
|
export { default as JogZPos } from "./JogZPos";
|
|
11
11
|
export { default as JogZNeg } from "./JogZNeg";
|
|
12
12
|
export { default as On } from "./On";
|
|
@@ -19,7 +19,7 @@ import { Distance, JogShort, JogMedium, JogLong } from '../assets';
|
|
|
19
19
|
import { Speed, SpeedSlow, SpeedMedium, SpeedFast } from '../assets';
|
|
20
20
|
import { RotationCcwA, RotationCcwB, RotationCcwC } from '../assets';
|
|
21
21
|
import { RotationCwA, RotationCwB, RotationCwC } from '../assets';
|
|
22
|
-
import { JogXLeft, JogXRight,
|
|
22
|
+
import { JogXLeft, JogXRight, JogYDownLeft, JogYUpRight, JogZPos, JogZNeg } from '../assets';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Enumerates the source button or action when an event occurs.
|
|
@@ -137,9 +137,9 @@ interface JogPanelState {
|
|
|
137
137
|
* A default jog button configuration for linear 3D motion.
|
|
138
138
|
*/
|
|
139
139
|
export const DefaultLinearJogButtons : (JogPanelButtonDefinition | undefined)[][] = [
|
|
140
|
-
[undefined, { icon: "jog_z_up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "
|
|
140
|
+
[undefined, { icon: "jog_z_up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "jog_y_up_right", action: JogPanelAction.yNegative, alt: "Y Negative" }],
|
|
141
141
|
[{ icon: "jog_x_left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "jog_x_right", action: JogPanelAction.xPositive, alt: "X Positive" }],
|
|
142
|
-
[{ icon: "
|
|
142
|
+
[{ icon: "jog_y_down_left", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "jog_z_down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
|
|
143
143
|
];
|
|
144
144
|
|
|
145
145
|
/**
|
|
@@ -272,8 +272,8 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
|
|
|
272
272
|
// Direction keys
|
|
273
273
|
case 'jog_x_left': return <JogXLeft />
|
|
274
274
|
case 'jog_x_right': return <JogXRight />
|
|
275
|
-
case 'jog_y_up_right': return <
|
|
276
|
-
case 'jog_y_down_left': return <
|
|
275
|
+
case 'jog_y_up_right': return <JogYUpRight />
|
|
276
|
+
case 'jog_y_down_left': return <JogYDownLeft />
|
|
277
277
|
case 'jog_z_up': return <JogZPos />
|
|
278
278
|
case 'jog_z_down': return <JogZNeg />
|
|
279
279
|
case 'rot_a_cw': return <RotationCwA />
|
|
@@ -288,8 +288,8 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
|
|
|
288
288
|
case 'pi pi-arrow-right': return <JogXRight />
|
|
289
289
|
case 'pi pi-arrow-up': return <JogZPos />
|
|
290
290
|
case 'pi pi-arrow-down': return <JogZNeg />
|
|
291
|
-
case 'pi pi-arrow-up-right': return <
|
|
292
|
-
case 'pi pi-arrow-down-left': return <
|
|
291
|
+
case 'pi pi-arrow-up-right': return <JogYUpRight />
|
|
292
|
+
case 'pi pi-arrow-down-left': return <JogYDownLeft />
|
|
293
293
|
|
|
294
294
|
default: return <i />
|
|
295
295
|
}
|
|
@@ -153,24 +153,28 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
|
|
|
153
153
|
}, []);
|
|
154
154
|
|
|
155
155
|
// -----------------------------------------------------------------
|
|
156
|
+
// Re-fetch the schema catalog. Called once on mount and again whenever
|
|
157
|
+
// the asset-TYPE editor broadcasts `ams.schema_changed` after a save, so
|
|
158
|
+
// open pickers/forms pick up new or edited types without a reload.
|
|
159
|
+
const loadSchemas = useCallback(async () => {
|
|
160
|
+
try {
|
|
161
|
+
const resp: any = await invoke('ams.list_schemas' as any, MessageType.Request, {} as any);
|
|
162
|
+
if (resp?.success && resp.data) {
|
|
163
|
+
setSchemas((resp.data.asset_types ?? {}) as AmsSchemaRegistry);
|
|
164
|
+
setSchemasLoaded(true);
|
|
165
|
+
} else {
|
|
166
|
+
console.warn('[AmsProvider] ams.list_schemas failed:', resp?.error_message);
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error('[AmsProvider] ams.list_schemas threw:', e);
|
|
170
|
+
}
|
|
171
|
+
}, [invoke]);
|
|
172
|
+
|
|
156
173
|
// Schema + role load — once on mount.
|
|
157
174
|
// -----------------------------------------------------------------
|
|
158
175
|
useEffect(() => {
|
|
159
176
|
let cancelled = false;
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const resp: any = await invoke('ams.list_schemas' as any, MessageType.Request, {} as any);
|
|
163
|
-
if (cancelled) return;
|
|
164
|
-
if (resp?.success && resp.data) {
|
|
165
|
-
setSchemas((resp.data.asset_types ?? {}) as AmsSchemaRegistry);
|
|
166
|
-
setSchemasLoaded(true);
|
|
167
|
-
} else {
|
|
168
|
-
console.warn('[AmsProvider] ams.list_schemas failed:', resp?.error_message);
|
|
169
|
-
}
|
|
170
|
-
} catch (e) {
|
|
171
|
-
console.error('[AmsProvider] ams.list_schemas threw:', e);
|
|
172
|
-
}
|
|
173
|
-
})();
|
|
177
|
+
void loadSchemas();
|
|
174
178
|
(async () => {
|
|
175
179
|
try {
|
|
176
180
|
const resp: any = await invoke('ams.list_roles' as any, MessageType.Request, {} as any);
|
|
@@ -214,12 +218,13 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
|
|
|
214
218
|
subscribe('ams.asset_changed', () => { refreshAssets(); }),
|
|
215
219
|
subscribe('ams.calibration_added', () => { refreshAssets(); }),
|
|
216
220
|
subscribe('ams.calibration_updated', () => { refreshAssets(); }),
|
|
221
|
+
subscribe('ams.schema_changed', () => { loadSchemas(); }),
|
|
217
222
|
subscribe('ams.asset_count', (v: any) => setAlerts(a => ({ ...a, assetCount: Number(v) || 0 }))),
|
|
218
223
|
subscribe('ams.alert_calibration_overdue', (v: any) => setAlerts(a => ({ ...a, calibrationOverdue: Number(v) || 0 }))),
|
|
219
224
|
subscribe('ams.alert_lane_unavailable', (v: any) => setAlerts(a => ({ ...a, laneUnavailable: Number(v) || 0 }))),
|
|
220
225
|
];
|
|
221
226
|
return () => { subs.forEach(unsubscribe); };
|
|
222
|
-
}, [subscribe, unsubscribe, refreshAssets]);
|
|
227
|
+
}, [subscribe, unsubscribe, refreshAssets, loadSchemas]);
|
|
223
228
|
|
|
224
229
|
// -----------------------------------------------------------------
|
|
225
230
|
// RPC helpers
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssetFieldArray — add/remove/reorder editor for a plain `TestField[]`.
|
|
3
|
+
*
|
|
4
|
+
* A decoupled sibling of the TIS `FieldArrayEditor` (which is bound to a
|
|
5
|
+
* `TestMethod` keyed by `FieldArrayKey`). Asset types carry bare field
|
|
6
|
+
* lists (`fields`, `calibration_fields`, and the per-key lists inside a
|
|
7
|
+
* keyed `sub_locations`), so this operates directly on an array + onChange.
|
|
8
|
+
*
|
|
9
|
+
* Reuses the TIS `TestFieldDialog` for the per-field modal — asset fields
|
|
10
|
+
* have no cross-test aggregate concept, so `isResultsField` is left false.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { DataTable } from 'primereact/datatable';
|
|
15
|
+
import { Column } from 'primereact/column';
|
|
16
|
+
import { Button } from 'primereact/button';
|
|
17
|
+
import { FormSection } from '../forms/FormSection';
|
|
18
|
+
import { TestFieldDialog } from '../tis-editor/editor/TestFieldDialog';
|
|
19
|
+
import type { TestField } from '../tis-editor/types';
|
|
20
|
+
|
|
21
|
+
export interface AssetFieldArrayProps {
|
|
22
|
+
title: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
fields: TestField[];
|
|
25
|
+
onChange: (next: TestField[]) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const AssetFieldArray: React.FC<AssetFieldArrayProps> = ({
|
|
29
|
+
title, description, fields, onChange,
|
|
30
|
+
}) => {
|
|
31
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
32
|
+
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
|
33
|
+
|
|
34
|
+
const handleAdd = () => { setEditingIdx(null); setDialogOpen(true); };
|
|
35
|
+
const handleEdit = (idx: number) => { setEditingIdx(idx); setDialogOpen(true); };
|
|
36
|
+
|
|
37
|
+
const handleSaveField = (f: TestField) => {
|
|
38
|
+
const next = [...fields];
|
|
39
|
+
if (editingIdx === null) next.push(f);
|
|
40
|
+
else next[editingIdx] = f;
|
|
41
|
+
onChange(next);
|
|
42
|
+
setDialogOpen(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleRemove = (idx: number) => {
|
|
46
|
+
const target = fields[idx];
|
|
47
|
+
if (!target) return;
|
|
48
|
+
if (!window.confirm(`Remove field "${target.name}"?`)) return;
|
|
49
|
+
onChange(fields.filter((_, i) => i !== idx));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleMove = (idx: number, dir: -1 | 1) => {
|
|
53
|
+
const j = idx + dir;
|
|
54
|
+
if (j < 0 || j >= fields.length) return;
|
|
55
|
+
const next = [...fields];
|
|
56
|
+
[next[idx], next[j]] = [next[j], next[idx]];
|
|
57
|
+
onChange(next);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const rowActions = (_row: TestField, opts: { rowIndex: number }) => (
|
|
61
|
+
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
62
|
+
<Button icon="pi pi-arrow-up" className="p-button-text p-button-sm"
|
|
63
|
+
disabled={opts.rowIndex === 0}
|
|
64
|
+
onClick={() => handleMove(opts.rowIndex, -1)} aria-label="Move up" />
|
|
65
|
+
<Button icon="pi pi-arrow-down" className="p-button-text p-button-sm"
|
|
66
|
+
disabled={opts.rowIndex === fields.length - 1}
|
|
67
|
+
onClick={() => handleMove(opts.rowIndex, 1)} aria-label="Move down" />
|
|
68
|
+
<Button icon="pi pi-pencil" className="p-button-text p-button-sm"
|
|
69
|
+
onClick={() => handleEdit(opts.rowIndex)} aria-label="Edit" />
|
|
70
|
+
<Button icon="pi pi-trash" className="p-button-text p-button-danger p-button-sm"
|
|
71
|
+
onClick={() => handleRemove(opts.rowIndex)} aria-label="Remove" />
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<FormSection
|
|
78
|
+
title={title}
|
|
79
|
+
description={description}
|
|
80
|
+
actions={<Button label="Add field" icon="pi pi-plus" size="small" onClick={handleAdd} />}
|
|
81
|
+
>
|
|
82
|
+
<DataTable value={fields} dataKey="name" emptyMessage="No fields defined.">
|
|
83
|
+
<Column field="name" header="Name" />
|
|
84
|
+
<Column field="type" header="Type" style={{ width: '6rem' }} />
|
|
85
|
+
<Column field="units" header="Units" style={{ width: '6rem' }} />
|
|
86
|
+
<Column header="Req" body={(r: TestField) => r.required ? '✓' : ''} style={{ width: '4rem' }} />
|
|
87
|
+
<Column field="label" header="Label" />
|
|
88
|
+
<Column header="" body={rowActions} style={{ width: '9rem' }} />
|
|
89
|
+
</DataTable>
|
|
90
|
+
</FormSection>
|
|
91
|
+
<TestFieldDialog
|
|
92
|
+
visible={dialogOpen}
|
|
93
|
+
initial={editingIdx !== null ? (fields[editingIdx] ?? null) : null}
|
|
94
|
+
siblingNames={fields.map(f => f.name)}
|
|
95
|
+
isResultsField={false}
|
|
96
|
+
onCancel={() => setDialogOpen(false)}
|
|
97
|
+
onSave={handleSaveField}
|
|
98
|
+
/>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default AssetFieldArray;
|
|
@@ -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;
|