Archives — panneau des fiches supprimées avec restauration
Bouton Archives dans la barre de filtres de la frise. Panneau latéral listant les fiches supprimées avec aperçu complet. Restauration recrée la fiche dans la frise. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+104
-10
@@ -604,6 +604,7 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-clear-filters" onclick="clearFilters()">Effacer</button>
|
||||
<button class="btn-history" onclick="openArchives()" style="margin-left:auto;">Archives</button>
|
||||
</div>
|
||||
<div class="frise-wrapper">
|
||||
<div class="timeline-area" id="timeline-area">
|
||||
@@ -3168,26 +3169,119 @@ function selectVersion(id) {
|
||||
|
||||
async function restoreVersion() {
|
||||
if (!selectedVersionId) return;
|
||||
if (!confirm('Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.')) return;
|
||||
const isArchive = versionType === 'archive';
|
||||
const msg = isArchive
|
||||
? 'Restaurer cette fiche supprimée ? Elle sera recréée dans la frise.'
|
||||
: 'Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.';
|
||||
if (!confirm(msg)) return;
|
||||
try {
|
||||
const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' });
|
||||
if (r.ok) {
|
||||
closeVersionPanel();
|
||||
if (versionType === 'frise') {
|
||||
if (isArchive) {
|
||||
// Re-create the card from snapshot
|
||||
const v = versionList.find(x => x.Id === selectedVersionId);
|
||||
if (!v) return;
|
||||
const snap = JSON.parse(v.Snapshot);
|
||||
const { Title, Acte, Ordre, Personnage, Tags, Notes } = snap;
|
||||
const data = { Title, Acte: Acte || 'En attente', Ordre: Ordre || 999, Personnage, Tags, Notes, 'Résumé': snap['Résumé'], 'Intérêt narratif': snap['Intérêt narratif'], Implications: snap.Implications, Origine: snap.Origine, 'Lien rédaction': snap['Lien rédaction'] };
|
||||
const r = await fetch('/api/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
||||
if (r.ok) {
|
||||
closeVersionPanel();
|
||||
await loadData();
|
||||
if (editingId) openEdit(editingId);
|
||||
} else if (versionType === 'redaction' && currentSceneId) {
|
||||
const scene = records.find(rec => rec.Id === currentSceneId);
|
||||
if (scene) openScene(scene);
|
||||
} else {
|
||||
alert('Erreur lors de la restauration');
|
||||
}
|
||||
} else {
|
||||
alert('Erreur lors de la restauration');
|
||||
const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' });
|
||||
if (r.ok) {
|
||||
closeVersionPanel();
|
||||
if (versionType === 'frise') {
|
||||
await loadData();
|
||||
if (editingId) openEdit(editingId);
|
||||
} else if (versionType === 'redaction' && currentSceneId) {
|
||||
const scene = records.find(rec => rec.Id === currentSceneId);
|
||||
if (scene) openScene(scene);
|
||||
}
|
||||
} else {
|
||||
alert('Erreur lors de la restauration');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Erreur : ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Archives (deleted cards) ──
|
||||
async function openArchives() {
|
||||
versionType = 'archive';
|
||||
versionRecordId = null;
|
||||
selectedVersionId = null;
|
||||
document.getElementById('version-panel-title').textContent = 'Archives — Fiches supprimées';
|
||||
document.getElementById('version-list').innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center;">Chargement...</div>';
|
||||
document.getElementById('version-preview').style.display = 'none';
|
||||
document.getElementById('version-actions').style.display = 'none';
|
||||
document.getElementById('version-panel').classList.add('open');
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/archives');
|
||||
const data = await r.json();
|
||||
versionList = data.list || [];
|
||||
renderArchiveList();
|
||||
} catch (e) {
|
||||
document.getElementById('version-list').innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center;">Erreur de chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchiveList() {
|
||||
const container = document.getElementById('version-list');
|
||||
if (versionList.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center;font-style:italic;">Aucune fiche supprimée.</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
for (const v of versionList) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'version-item';
|
||||
div.dataset.id = v.Id;
|
||||
const date = new Date(v.CreatedAt);
|
||||
const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
let title = '', preview = '';
|
||||
try {
|
||||
const snap = JSON.parse(v.Snapshot);
|
||||
title = snap.Title || snap.Titre || 'Sans titre';
|
||||
preview = [snap.Acte, snap['Résumé']].filter(Boolean).join(' — ');
|
||||
} catch (e) { title = 'Fiche'; }
|
||||
div.innerHTML = `<div class="version-item-date">${dateStr}</div>` +
|
||||
`<div class="version-item-label">${title.replace(/</g, '<')}</div>` +
|
||||
(preview ? `<div class="version-item-preview">${preview.replace(/</g, '<').substring(0, 200)}</div>` : '');
|
||||
div.onclick = () => selectArchive(v.Id);
|
||||
container.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function selectArchive(id) {
|
||||
selectedVersionId = id;
|
||||
versionType = 'archive';
|
||||
document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id));
|
||||
const v = versionList.find(x => x.Id === id);
|
||||
if (!v) return;
|
||||
try {
|
||||
const snap = JSON.parse(v.Snapshot);
|
||||
const content = document.getElementById('version-preview-content');
|
||||
const parts = [];
|
||||
if (snap.Title) parts.push(`<strong>${snap.Title.replace(/</g, '<')}</strong>`);
|
||||
if (snap.Acte) parts.push(`<em>${snap.Acte}</em>`);
|
||||
if (snap.Personnage) parts.push('Personnages : ' + snap.Personnage.replace(/</g, '<'));
|
||||
if (snap['Résumé']) parts.push(snap['Résumé'].replace(/</g, '<'));
|
||||
if (snap['Intérêt narratif']) parts.push('<strong>Intérêt :</strong> ' + snap['Intérêt narratif'].replace(/</g, '<'));
|
||||
if (snap.Tags) parts.push('<em>Tags : ' + snap.Tags.replace(/</g, '<') + '</em>');
|
||||
if (snap.Notes) parts.push('<strong>Notes :</strong> ' + snap.Notes.replace(/</g, '<'));
|
||||
content.innerHTML = parts.join('<br>');
|
||||
document.getElementById('version-preview').style.display = '';
|
||||
document.getElementById('version-actions').style.display = '';
|
||||
} catch (e) {
|
||||
document.getElementById('version-preview').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Close popup on version panel click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const panel = document.getElementById('version-panel');
|
||||
|
||||
@@ -385,6 +385,19 @@ app.post('/api/versions/:id/restore', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// List archived (deleted) frise cards
|
||||
app.get('/api/archives', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_VERSIONS_TABLE_ID) return res.json({ list: [] });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}?where=(Type,eq,frise)~and(Label,like,%25supprimé%25)&sort=-CreatedAt&limit=100`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const data = await r.json();
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Personnages API ──
|
||||
|
||||
function personnagesProxy(tableIdVar) {
|
||||
|
||||
Reference in New Issue
Block a user