Code-Schnipsel History: Aktuell ganz oben, korrekter Diff, History-Einträge löschbar
This commit is contained in:
@@ -98,6 +98,13 @@ router.put('/:id', authenticate, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Löschen ──────────────────────────────────────────────────────────────────
|
// ── Löschen ──────────────────────────────────────────────────────────────────
|
||||||
|
router.delete('/:id/history/:hid', authenticate, (req, res) => {
|
||||||
|
const s = db.prepare('SELECT * FROM snippets WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
||||||
|
if (!s) return res.status(403).json({ error: 'Kein Zugriff' });
|
||||||
|
db.prepare('DELETE FROM snippet_history WHERE id=? AND snippet_id=?').run(req.params.hid, s.id);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
router.delete('/:id', authenticate, (req, res) => {
|
router.delete('/:id', authenticate, (req, res) => {
|
||||||
const r = db.prepare('DELETE FROM snippets WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
const r = db.prepare('DELETE FROM snippets WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
||||||
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||||
|
|||||||
@@ -140,66 +140,124 @@ function DiffView({ oldCode, newCode }) {
|
|||||||
}
|
}
|
||||||
function HistoryModal({ snippet, onClose, onRestore, toast }) {
|
function HistoryModal({ snippet, onClose, onRestore, toast }) {
|
||||||
const [history, setHistory] = useState([]);
|
const [history, setHistory] = useState([]);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selectedIdx, setSelectedIdx] = useState(0); // 0 = Aktuell (current)
|
||||||
const [showDiff, setShowDiff] = useState(true);
|
const [showDiff, setShowDiff] = useState(true);
|
||||||
useEffect(()=>{
|
|
||||||
api(`/tools/snippets/${snippet.id}/history`).then(h=>{ setHistory(h); if(h.length) setSelected(h[0]); }).catch(()=>{});
|
|
||||||
},[]);
|
|
||||||
const isMob = window.innerWidth < 768;
|
const isMob = window.innerWidth < 768;
|
||||||
const getOlderCode = (h) => {
|
|
||||||
const idx = history.indexOf(h);
|
const load = () => api(`/tools/snippets/${snippet.id}/history`).then(setHistory).catch(()=>{});
|
||||||
// Newest history entry (idx=0) compares against CURRENT snippet code
|
useEffect(()=>{ load(); },[]);
|
||||||
if (idx === 0) return snippet.code;
|
|
||||||
// Older entries compare against the next newer history entry
|
// Entries: [current, ...history] where index 0 = current
|
||||||
return history[idx - 1].code;
|
// entries[i].code, entries[i].label
|
||||||
|
const entries = [
|
||||||
|
{ id:'current', code: snippet.code, language: snippet.language,
|
||||||
|
saved_at: snippet.updated_at, label:'Aktuell', isCurrent: true },
|
||||||
|
...history.map((h,i) => ({ ...h, label:`v${history.length - i}`, isCurrent: false })),
|
||||||
|
];
|
||||||
|
|
||||||
|
const selected = entries[selectedIdx];
|
||||||
|
// Diff: what changed FROM previous version TO selected (selected is newer)
|
||||||
|
const prevEntry = entries[selectedIdx + 1]; // one step older
|
||||||
|
const oldCode = prevEntry?.code ?? null; // null = älteste, kein Diff
|
||||||
|
|
||||||
|
const delHistory = async (hid) => {
|
||||||
|
try {
|
||||||
|
await api(`/tools/snippets/${snippet.id}/history/${hid}`, { method:'DELETE' });
|
||||||
|
await load();
|
||||||
|
// If deleted entry was selected, jump to current
|
||||||
|
setSelectedIdx(0);
|
||||||
|
toast('Version gelöscht');
|
||||||
|
} catch(e) { toast(e.message,'error'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
|
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
|
||||||
display:'flex',alignItems:isMob?'flex-end':'center',justifyContent:'center',padding:isMob?0:24}}
|
display:'flex',alignItems:isMob?'flex-end':'center',justifyContent:'center',padding:isMob?0:24}}
|
||||||
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
||||||
<div style={{background:'#1a1a1e',borderRadius:isMob?'16px 16px 0 0':14,
|
<div style={{background:'#1a1a1e',borderRadius:isMob?'16px 16px 0 0':14,
|
||||||
width:'100%',maxWidth:680,border:'1px solid rgba(255,255,255,0.12)',
|
width:'100%',maxWidth:700,border:'1px solid rgba(255,255,255,0.12)',
|
||||||
display:'flex',flexDirection:'column',maxHeight:isMob?'88vh':'82vh',overflow:'hidden'}}>
|
display:'flex',flexDirection:'column',maxHeight:isMob?'88vh':'82vh',overflow:'hidden'}}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div style={{padding:'14px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',
|
<div style={{padding:'14px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',
|
||||||
display:'flex',justifyContent:'space-between',alignItems:'center',flexShrink:0,gap:10}}>
|
display:'flex',justifyContent:'space-between',alignItems:'center',flexShrink:0,gap:10}}>
|
||||||
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>📜 Versionshistorie</div>
|
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
|
||||||
|
📜 Versionshistorie · {snippet.title}
|
||||||
|
</div>
|
||||||
<div style={{display:'flex',gap:6,alignItems:'center'}}>
|
<div style={{display:'flex',gap:6,alignItems:'center'}}>
|
||||||
<button onClick={()=>setShowDiff(v=>!v)} style={{
|
<button onClick={()=>setShowDiff(v=>!v)} style={{
|
||||||
background:showDiff?'rgba(78,205,196,0.12)':'rgba(255,255,255,0.05)',
|
background:showDiff?'rgba(78,205,196,0.12)':'rgba(255,255,255,0.05)',
|
||||||
border:`1px solid ${showDiff?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
|
border:`1px solid ${showDiff?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
|
||||||
borderRadius:6,color:showDiff?'#4ecdc4':'rgba(255,255,255,0.4)',
|
borderRadius:6,color:showDiff?'#4ecdc4':'rgba(255,255,255,0.4)',
|
||||||
cursor:'pointer',fontSize:9,fontFamily:'monospace',padding:'3px 8px'}}>
|
cursor:'pointer',fontSize:9,fontFamily:'monospace',padding:'3px 8px'}}>
|
||||||
{showDiff?'± Diff':'Code'}
|
{showDiff?'± Diff':'📄 Code'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}>✕</button>
|
<button onClick={onClose} style={{background:'transparent',border:'none',
|
||||||
|
color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{history.length===0 ? (
|
|
||||||
<div style={{padding:32,textAlign:'center',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>Keine älteren Versionen vorhanden.</div>
|
{entries.length <= 1 ? (
|
||||||
|
<div style={{padding:32,textAlign:'center',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>
|
||||||
|
Keine älteren Versionen vorhanden.
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{display:'flex',flex:1,overflow:'hidden'}}>
|
<div style={{display:'flex',flex:1,overflow:'hidden'}}>
|
||||||
<div style={{width:150,borderRight:'1px solid rgba(255,255,255,0.07)',overflowY:'auto',flexShrink:0}}>
|
|
||||||
{history.map((h,i)=>(
|
{/* Versions-Liste */}
|
||||||
<button key={h.id} onClick={()=>setSelected(h)} style={{
|
<div style={{width:155,borderRight:'1px solid rgba(255,255,255,0.07)',overflowY:'auto',flexShrink:0}}>
|
||||||
width:'100%',padding:'10px 12px',
|
{entries.map((e,i) => (
|
||||||
background:selected?.id===h.id?'rgba(78,205,196,0.1)':'transparent',
|
<div key={e.id} style={{
|
||||||
borderLeft:`2px solid ${selected?.id===h.id?'#4ecdc4':'transparent'}`,
|
display:'flex',alignItems:'center',
|
||||||
border:'none',borderRight:'none',borderTop:'none',borderBottom:'1px solid rgba(255,255,255,0.05)',
|
background: i===selectedIdx?'rgba(78,205,196,0.08)':'transparent',
|
||||||
cursor:'pointer',textAlign:'left'}}>
|
borderLeft:`2px solid ${i===selectedIdx?'#4ecdc4':'transparent'}`,
|
||||||
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:9}}>v{history.length-i}</div>
|
borderBottom:'1px solid rgba(255,255,255,0.04)',
|
||||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:8,marginTop:2}}>{fmtDt(h.saved_at)}</div>
|
}}>
|
||||||
</button>
|
<button onClick={()=>setSelectedIdx(i)} style={{
|
||||||
|
flex:1,padding:'10px 10px',background:'transparent',
|
||||||
|
border:'none',cursor:'pointer',textAlign:'left',
|
||||||
|
}}>
|
||||||
|
<div style={{color: e.isCurrent?'#4ecdc4':'rgba(255,255,255,0.65)',
|
||||||
|
fontFamily:'monospace',fontSize:10,fontWeight: e.isCurrent?700:400}}>
|
||||||
|
{e.label}
|
||||||
|
{e.isCurrent&&<span style={{color:'rgba(78,205,196,0.5)',fontSize:8,marginLeft:4}}>●</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:8,marginTop:2}}>
|
||||||
|
{fmtDt(e.saved_at)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/* Löschen nur für Altversionen, nicht Aktuell */}
|
||||||
|
{!e.isCurrent && !snippet.is_shared && (
|
||||||
|
<button onClick={()=>delHistory(e.id)}
|
||||||
|
title="Version löschen"
|
||||||
|
style={{background:'transparent',border:'none',cursor:'pointer',
|
||||||
|
padding:'0 8px',color:'rgba(255,107,157,0.35)',fontSize:12,
|
||||||
|
flexShrink:0,lineHeight:1}}>✕</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Detail */}
|
||||||
<div style={{flex:1,overflow:'auto',padding:14,display:'flex',flexDirection:'column',gap:10}}>
|
<div style={{flex:1,overflow:'auto',padding:14,display:'flex',flexDirection:'column',gap:10}}>
|
||||||
{selected && (
|
{selected && (
|
||||||
<>
|
<>
|
||||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',flexWrap:'wrap',gap:6}}>
|
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',flexWrap:'wrap',gap:6}}>
|
||||||
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>
|
<div style={{display:'flex',gap:6,alignItems:'center',flexWrap:'wrap'}}>
|
||||||
{getLangLabel(selected.language)} · {fmtDt(selected.saved_at)}
|
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>
|
||||||
{history.indexOf(selected) === history.length-1 && <span style={{color:'rgba(255,255,255,0.2)',marginLeft:6}}>(älteste Version)</span>}
|
{getLangLabel(selected.language)} · {fmtDt(selected.saved_at)}
|
||||||
</span>
|
</span>
|
||||||
{!snippet.is_shared && selected.code !== snippet.code && (
|
{showDiff && oldCode===null && (
|
||||||
|
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9}}>älteste Version – kein Diff verfügbar</span>
|
||||||
|
)}
|
||||||
|
{showDiff && oldCode!==null && prevEntry && (
|
||||||
|
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9}}>
|
||||||
|
Änderungen gegenüber {prevEntry.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Wiederherstellen nur für Altversionen die vom aktuellen abweichen */}
|
||||||
|
{!selected.isCurrent && !snippet.is_shared && selected.code !== snippet.code && (
|
||||||
<button onClick={()=>onRestore(selected)} style={{
|
<button onClick={()=>onRestore(selected)} style={{
|
||||||
background:'rgba(255,230,109,0.1)',border:'1px solid rgba(255,230,109,0.25)',
|
background:'rgba(255,230,109,0.1)',border:'1px solid rgba(255,230,109,0.25)',
|
||||||
borderRadius:6,color:'#ffe66d',cursor:'pointer',fontSize:10,padding:'4px 10px'}}>
|
borderRadius:6,color:'#ffe66d',cursor:'pointer',fontSize:10,padding:'4px 10px'}}>
|
||||||
@@ -207,11 +265,14 @@ function HistoryModal({ snippet, onClose, onRestore, toast }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showDiff
|
|
||||||
? <DiffView oldCode={getOlderCode(selected)} newCode={selected.code}/>
|
{showDiff && oldCode!==null
|
||||||
: <pre style={{margin:0,padding:'12px 14px',color:'#d0d0d0',fontFamily:"'Courier New',monospace",
|
? <DiffView oldCode={oldCode} newCode={selected.code}/>
|
||||||
fontSize:11,lineHeight:1.6,background:'rgba(0,0,0,0.35)',border:'1px solid rgba(255,255,255,0.08)',
|
: <pre style={{margin:0,padding:'12px 14px',color:'#d0d0d0',
|
||||||
borderRadius:8,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-all',maxHeight:320,overflowY:'auto'}}>
|
fontFamily:"'Courier New',monospace",fontSize:11,lineHeight:1.6,
|
||||||
|
background:'rgba(0,0,0,0.35)',border:'1px solid rgba(255,255,255,0.08)',
|
||||||
|
borderRadius:8,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-all',
|
||||||
|
maxHeight:380,overflowY:'auto'}}>
|
||||||
{selected.code}
|
{selected.code}
|
||||||
</pre>
|
</pre>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user