@adcops/autocore-react 3.3.112 → 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/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/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
|
@@ -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;
|