Skip to content

Commit 8f82e30

Browse files
authored
Merge pull request #62 from vitruv-tools/change-save-vsum-with-new-api
update vsum sync api
2 parents b31423a + 9e53450 commit 8f82e30

File tree

3 files changed

+167
-178
lines changed

3 files changed

+167
-178
lines changed

src/components/ui/SidebarTabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export const SidebarTabs: React.FC<SidebarTabsProps> = ({ width = 350, showBorde
7373
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: 16, color: '#6b7280' }}>
7474
<div style={{ fontWeight: 700, color: '#111827', marginBottom: 8 }}>Choose a workspace</div>
7575
<div style={{ fontSize: 13, lineHeight: 1.6 }}>
76-
Select <span style={{ fontWeight: 700 }}>MML</span> to browse and upload Meta Models, or
77-
select <span style={{ fontWeight: 700 }}>Project</span> to open vSUMS project tools.
76+
Select <span style={{ fontWeight: 700 }}>Meta Model</span> to browse and upload Meta Models, or
77+
select <span style={{ fontWeight: 700 }}>Projects</span> to open vSUMS project tools.
7878
</div>
7979
</div>
8080
)}

src/components/ui/VsumTabs.tsx

Lines changed: 149 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useRef, useState } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import { VsumDetails } from '../../types';
33
import { apiService } from '../../services/api';
44

@@ -12,11 +12,8 @@ interface VsumTabsProps {
1212
export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onActivate, onClose }) => {
1313
const [detailsById, setDetailsById] = useState<Record<number, VsumDetails | undefined>>({});
1414
const [error, setError] = useState<string>('');
15-
const [edits, setEdits] = useState<Record<number, { name: string; metaModelIds: number[] }>>({});
15+
const [edits, setEdits] = useState<Record<number, { metaModelIds: number[] }>>({});
1616
const [saving, setSaving] = useState(false);
17-
const [renamingId, setRenamingId] = useState<number | null>(null);
18-
const [renameValue, setRenameValue] = useState<string>('');
19-
const renameCommitRef = useRef(false);
2017

2118
const areIdArraysEqual = (a: number[] = [], b: number[] = []) => {
2219
if (a.length !== b.length) return false;
@@ -35,8 +32,7 @@ export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onA
3532
const details = detailsById[id];
3633
if (!edit || !details) { map[id] = false; return; }
3734
const detailsIds = (details.metaModels || []).map(m => m.id);
38-
const isDirty = edit.name.trim() !== (details.name || '').trim() || !areIdArraysEqual(edit.metaModelIds, detailsIds);
39-
map[id] = isDirty;
35+
map[id] = !areIdArraysEqual(edit.metaModelIds, detailsIds);
4036
});
4137
return map;
4238
}, [openVsums, edits, detailsById]);
@@ -47,7 +43,10 @@ export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onA
4743
try {
4844
const res = await apiService.getVsumDetails(id);
4945
setDetailsById(prev => ({ ...prev, [id]: res.data }));
50-
setEdits(prev => ({ ...prev, [id]: { name: res.data.name, metaModelIds: (res.data.metaModels || []).map(m => m.id) } }));
46+
setEdits(prev => ({
47+
...prev,
48+
[id]: { metaModelIds: (res.data.metaModels || []).map(m => m.id) }
49+
}));
5150
} catch (e) {
5251
setError(e instanceof Error ? e.message : 'Failed to load VSUM details');
5352
}
@@ -57,21 +56,38 @@ export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onA
5756
}
5857
}, [activeVsumId, detailsById]);
5958

60-
// Derived states no longer needed: activeEdit, activeDetails
61-
62-
const saveById = async (id: number, override?: { name?: string; metaModelIds?: number[] }) => {
59+
const saveById = async (
60+
id: number,
61+
override?: { metaModelIds?: number[] }
62+
) => {
6363
const edit = edits[id];
6464
const fallbackMetaIds = (detailsById[id]?.metaModels || []).map(m => m.id);
65-
const name = (override?.name ?? edit?.name ?? '').trim();
6665
const metaModelIds = override?.metaModelIds ?? edit?.metaModelIds ?? fallbackMetaIds;
67-
if (!name) { setError('Name is required'); return; }
66+
67+
if (!metaModelIds || metaModelIds.length === 0) {
68+
setError('At least one MetaModel is required');
69+
return;
70+
}
71+
6872
setSaving(true);
6973
setError('');
7074
try {
71-
await apiService.updateVsum(id, { name, metaModelIds });
75+
await apiService.updateVsumSyncChanges(id, {
76+
metaModelIds,
77+
metaModelRelationRequests: null, // not implemented yet
78+
});
79+
7280
const res = await apiService.getVsumDetails(id);
7381
setDetailsById(prev => ({ ...prev, [id]: res.data }));
74-
setEdits(prev => ({ ...prev, [id]: { name: res.data.name, metaModelIds: (res.data.metaModels || []).map(m => m.id) } }));
82+
83+
// keep edit state in sync with server
84+
setEdits(prev => ({
85+
...prev,
86+
[id]: {
87+
metaModelIds: (res.data.metaModels || []).map(m => m.id),
88+
},
89+
}));
90+
7591
window.dispatchEvent(new CustomEvent('vitruv.refreshVsums'));
7692
} catch (e) {
7793
setError(e instanceof Error ? e.message : 'Failed to save VSUM');
@@ -85,45 +101,23 @@ export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onA
85101
await saveById(activeVsumId);
86102
};
87103

88-
const ensureDetails = async (id: number) => {
89-
if (detailsById[id]) return detailsById[id];
90-
try {
91-
const res = await apiService.getVsumDetails(id);
92-
setDetailsById(prev => ({ ...prev, [id]: res.data }));
93-
setEdits(prev => ({ ...prev, [id]: { name: res.data.name, metaModelIds: (res.data.metaModels || []).map(m => m.id) } }));
94-
return res.data;
95-
} catch (e) {
96-
setError(e instanceof Error ? e.message : 'Failed to load VSUM details');
97-
return undefined;
98-
}
99-
};
100-
101-
const beginRename = async (id: number) => {
102-
await ensureDetails(id);
103-
const currentName = edits[id]?.name || detailsById[id]?.name || `VSUM #${id}`;
104-
setRenamingId(id);
105-
setRenameValue(currentName);
106-
};
107-
108-
const commitRenameById = async (id: number) => {
109-
const current = edits[id] || { name: renameValue, metaModelIds: (detailsById[id]?.metaModels || []).map(m => m.id) };
110-
const nextName = renameValue.trim();
111-
if (!nextName) { setError('Name is required'); return; }
112-
setEdits(prev => ({ ...prev, [id]: { ...current, name: nextName } }));
113-
setRenamingId(null);
114-
await saveById(id, { name: nextName });
115-
};
116-
104+
// handle external "add meta model" event
117105
useEffect(() => {
118106
const onAdd = (e: Event) => {
119107
const ce = e as CustomEvent<{ id: number }>; // meta model id
120108
if (!activeVsumId) return;
121109
const mmId = ce.detail?.id;
122110
if (typeof mmId !== 'number') return;
123-
const current = edits[activeVsumId] || { name: detailsById[activeVsumId!]?.name || '', metaModelIds: [] };
111+
112+
const current = edits[activeVsumId] || { metaModelIds: (detailsById[activeVsumId!]?.metaModels || []).map(m => m.id) };
124113
if (current.metaModelIds.includes(mmId)) return;
125-
setEdits(prev => ({ ...prev, [activeVsumId!]: { ...current, metaModelIds: [...current.metaModelIds, mmId] } }));
114+
115+
setEdits(prev => ({
116+
...prev,
117+
[activeVsumId!]: { metaModelIds: [...current.metaModelIds, mmId] }
118+
}));
126119
};
120+
127121
window.addEventListener('vitruv.addMetaModelToActiveVsum', onAdd as EventListener);
128122
return () => window.removeEventListener('vitruv.addMetaModelToActiveVsum', onAdd as EventListener);
129123
}, [activeVsumId, edits, detailsById]);
@@ -133,134 +127,115 @@ export const VsumTabs: React.FC<VsumTabsProps> = ({ openVsums, activeVsumId, onA
133127
const anyDirty = activeVsumId ? !!dirtyById[activeVsumId] : false;
134128

135129
return (
136-
<div style={{ background: '#ffffff', borderBottom: '1px solid #e5e7eb', position: 'sticky', top: 0, zIndex: 20, boxShadow: 'inset 0 -1px 0 #e5e7eb', cursor: saving ? 'progress' : 'default' }}>
137-
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px' }}>
138-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, overflowX: 'auto', flex: 1 }}>
139-
{openVsums.map(id => {
140-
const isActive = id === activeVsumId;
141-
const name = detailsById[id]?.name || `VSUM #${id}`;
142-
const isDirty = !!dirtyById[id];
143-
return (
144-
<div
145-
key={id}
146-
style={{
147-
display: 'flex', alignItems: 'center', gap: 8,
148-
padding: '6px 12px',
149-
border: isActive ? '1px solid #bfdbfe' : '1px solid #e5e7eb',
150-
borderBottom: isActive ? '2px solid #3b82f6' : '1px solid #e5e7eb',
151-
borderRadius: 8,
152-
background: isActive ? '#f0f7ff' : '#ffffff',
153-
cursor: 'pointer',
154-
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,0.04)' : 'none'
155-
}}
156-
onClick={() => onActivate(id)}
157-
onContextMenu={(e) => { e.preventDefault(); beginRename(id); }}
158-
aria-current={isActive ? 'page' : undefined}
159-
title={name}
160-
>
161-
{isDirty && <span aria-label="Unsaved changes" title="Unsaved changes" style={{ width: 6, height: 6, borderRadius: 6, background: '#f59e0b', display: 'inline-block' }} />}
162-
{renamingId === id ? (
163-
<input
164-
value={renameValue}
165-
onChange={(e) => setRenameValue(e.target.value)}
166-
onKeyDown={(e) => {
167-
if (e.key === 'Enter') {
168-
e.preventDefault();
169-
renameCommitRef.current = true;
170-
void (async () => {
171-
try {
172-
await commitRenameById(id);
173-
} finally {
174-
// allow blur after commit
175-
setTimeout(() => { renameCommitRef.current = false; }, 0);
176-
}
177-
})();
178-
}
179-
if (e.key === 'Escape') { setRenamingId(null); }
180-
}}
181-
onBlur={() => { if (!renameCommitRef.current) { setRenamingId(null); } }}
182-
autoFocus
183-
disabled={saving}
184-
style={{
185-
padding: '1px 4px',
186-
border: 'none',
187-
borderBottom: '1px solid #94a3b8',
188-
background: 'transparent',
189-
fontSize: 12,
190-
minWidth: 80,
191-
color: '#111827',
192-
outline: 'none'
193-
}}
194-
/>
195-
) : (
196-
<span style={{ fontWeight: 700, color: '#1f2937', fontSize: 12, whiteSpace: 'nowrap' }}>{name}</span>
197-
)}
198-
{renamingId === id && (
199-
<button
200-
onMouseDown={() => { renameCommitRef.current = true; }}
201-
onClick={(e) => {
202-
e.stopPropagation();
203-
void (async () => {
204-
try {
205-
await commitRenameById(id);
206-
} finally {
207-
setTimeout(() => { renameCommitRef.current = false; }, 0);
208-
}
209-
})();
210-
}}
211-
disabled={saving || !renameValue.trim() || renameValue.trim() === (edits[id]?.name || detailsById[id]?.name || '')}
212-
style={{ border: '1px solid transparent', background: 'transparent', color: saving ? '#93c5fd' : '#3b82f6', cursor: saving ? 'not-allowed' : 'pointer', borderRadius: 4, lineHeight: 1, width: 16, height: 16, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
213-
aria-label={`Save name for ${name}`}
214-
title="Save"
215-
>
216-
217-
</button>
218-
)}
219-
<button
220-
onMouseDown={() => { renameCommitRef.current = true; }}
221-
onClick={(e) => {
222-
e.stopPropagation();
223-
setRenamingId(null);
224-
onClose(id);
225-
setTimeout(() => { renameCommitRef.current = false; }, 0);
226-
}}
227-
style={{ border: '1px solid transparent', background: 'transparent', color: '#64748b', cursor: 'pointer', borderRadius: 4, lineHeight: 1, width: 16, height: 16, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
228-
aria-label={`Close ${name}`}
229-
title="Close"
230-
>
231-
×
232-
</button>
233-
</div>
234-
);
235-
})}
236-
</div>
237-
{activeVsumId && (
238-
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
239-
{error && (
240-
<div role="alert" style={{ padding: '4px 8px', border: '1px solid #fecaca', color: '#991b1b', background: '#fef2f2', borderRadius: 6, fontSize: 11 }}>{error}</div>
241-
)}
242-
{anyDirty && (
243-
<button
244-
onClick={onSave}
245-
disabled={saving}
246-
style={{
247-
padding: '6px 10px',
248-
border: '1px solid #3b82f6',
249-
borderRadius: 8,
250-
background: saving ? '#bfdbfe' : '#3b82f6',
251-
color: '#ffffff',
252-
fontWeight: 700,
253-
cursor: saving ? 'not-allowed' : 'pointer'
254-
}}
255-
>
256-
{saving ? 'Saving…' : 'Save changes'}
257-
</button>
258-
)}
130+
<div
131+
style={{
132+
background: '#ffffff',
133+
borderBottom: '1px solid #e5e7eb',
134+
position: 'sticky',
135+
top: 0,
136+
zIndex: 20,
137+
boxShadow: 'inset 0 -1px 0 #e5e7eb',
138+
cursor: saving ? 'progress' : 'default'
139+
}}
140+
>
141+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px' }}>
142+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, overflowX: 'auto', flex: 1 }}>
143+
{openVsums.map(id => {
144+
const isActive = id === activeVsumId;
145+
const name = detailsById[id]?.name || `VSUM #${id}`;
146+
const isDirty = !!dirtyById[id];
147+
return (
148+
<div
149+
key={id}
150+
style={{
151+
display: 'flex',
152+
alignItems: 'center',
153+
gap: 8,
154+
padding: '6px 12px',
155+
border: isActive ? '1px solid #bfdbfe' : '1px solid #e5e7eb',
156+
borderBottom: isActive ? '2px solid #3b82f6' : '1px solid #e5e7eb',
157+
borderRadius: 8,
158+
background: isActive ? '#f0f7ff' : '#ffffff',
159+
cursor: 'pointer',
160+
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,0.04)' : 'none'
161+
}}
162+
onClick={() => onActivate(id)}
163+
aria-current={isActive ? 'page' : undefined}
164+
title={name}
165+
>
166+
{isDirty && (
167+
<span
168+
aria-label="Unsaved changes"
169+
title="Unsaved changes"
170+
style={{ width: 6, height: 6, borderRadius: 6, background: '#f59e0b', display: 'inline-block' }}
171+
/>
172+
)}
173+
<span style={{ fontWeight: 700, color: '#1f2937', fontSize: 12, whiteSpace: 'nowrap' }}>{name}</span>
174+
<button
175+
onClick={(e) => {
176+
e.stopPropagation();
177+
onClose(id);
178+
}}
179+
style={{
180+
border: '1px solid transparent',
181+
background: 'transparent',
182+
color: '#64748b',
183+
cursor: 'pointer',
184+
borderRadius: 4,
185+
lineHeight: 1,
186+
width: 16,
187+
height: 16,
188+
display: 'inline-flex',
189+
alignItems: 'center',
190+
justifyContent: 'center'
191+
}}
192+
aria-label={`Close ${name}`}
193+
title="Close"
194+
>
195+
×
196+
</button>
197+
</div>
198+
);
199+
})}
259200
</div>
260-
)}
201+
202+
{activeVsumId && (
203+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
204+
{error && (
205+
<div
206+
role="alert"
207+
style={{
208+
padding: '4px 8px',
209+
border: '1px solid #fecaca',
210+
color: '#991b1b',
211+
background: '#fef2f2',
212+
borderRadius: 6,
213+
fontSize: 11
214+
}}
215+
>
216+
{error}
217+
</div>
218+
)}
219+
{anyDirty && (
220+
<button
221+
onClick={onSave}
222+
disabled={saving}
223+
style={{
224+
padding: '6px 10px',
225+
border: '1px solid #3b82f6',
226+
borderRadius: 8,
227+
background: saving ? '#bfdbfe' : '#3b82f6',
228+
color: '#ffffff',
229+
fontWeight: 700,
230+
cursor: saving ? 'not-allowed' : 'pointer'
231+
}}
232+
>
233+
{saving ? 'Saving…' : 'Save changes'}
234+
</button>
235+
)}
236+
</div>
237+
)}
238+
</div>
261239
</div>
262-
</div>
263240
);
264-
};
265-
266-
241+
};

0 commit comments

Comments
 (0)