Historique des versions — frise + rédaction
Table NocoDB Versions (Type, RecordId, Snapshot, Label). Snapshots auto avant chaque modification de fiche frise et de texte rédigé. Archive avant suppression de fiche. Panneau latéral avec aperçu et restauration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+170
-3
@@ -417,7 +417,7 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
||||
.binder-scene.active .scene-words { color: rgba(255,255,255,0.6); }
|
||||
|
||||
.editor-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.editor-header { padding: 20px 32px 12px; border-bottom: 1px solid var(--border-light); }
|
||||
.editor-header { padding: 20px 32px 12px; border-bottom: 1px solid var(--border-light); display: flex; align-items: flex-start; gap: 12px; }
|
||||
.editor-scene-title { font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 600; color: var(--text); }
|
||||
.editor-scene-meta { font-size: 13px; color: var(--text-hint); margin-top: 4px; display: flex; gap: 16px; }
|
||||
.editor-body { flex: 1; overflow-y: auto; padding: 24px 32px 80px; max-width: 720px; }
|
||||
@@ -468,6 +468,26 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
||||
.dark .comment-mark.comment-e { background: rgba(138,170,90,0.2); border-bottom-color: rgba(138,170,90,0.4); }
|
||||
.dark .comment-mark.comment-m { background: rgba(212,132,90,0.2); border-bottom-color: rgba(212,132,90,0.4); }
|
||||
|
||||
/* ── Version history panel ── */
|
||||
.version-panel { position: fixed; top: 0; right: -420px; width: 400px; height: 100vh; background: var(--bg-card); border-left: 1px solid var(--border); z-index: 250; transition: right 0.3s ease; display: flex; flex-direction: column; box-shadow: -4px 0 20px var(--shadow); }
|
||||
.version-panel.open { right: 0; }
|
||||
.version-panel-header { padding: 20px 24px 16px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; }
|
||||
.version-panel-header h3 { font-family: 'Playfair Display', serif; font-size: 16px; font-weight: 600; }
|
||||
.version-panel-list { flex: 1; overflow-y: auto; padding: 12px; }
|
||||
.version-item { padding: 14px 16px; border: 1px solid var(--border-light); border-radius: 10px; margin-bottom: 10px; cursor: pointer; transition: border-color 0.2s; }
|
||||
.version-item:hover { border-color: var(--accent); }
|
||||
.version-item.selected { border-color: var(--accent); background: var(--bg-alt); }
|
||||
.version-item-date { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
|
||||
.version-item-label { font-size: 14px; font-weight: 500; margin-bottom: 6px; }
|
||||
.version-item-preview { font-size: 12px; color: var(--text-hint); line-height: 1.5; max-height: 60px; overflow: hidden; }
|
||||
.version-preview-pane { padding: 16px 24px; border-top: 1px solid var(--border-light); max-height: 40vh; overflow-y: auto; }
|
||||
.version-preview-pane h4 { font-family: 'Playfair Display', serif; font-size: 14px; margin-bottom: 10px; color: var(--text-muted); }
|
||||
.version-preview-content { font-size: 13px; line-height: 1.6; }
|
||||
.version-actions { padding: 12px 24px 20px; border-top: 1px solid var(--border-light); display: flex; gap: 10px; }
|
||||
.version-actions button { flex: 1; }
|
||||
.btn-history { background: none; border: 1px solid var(--border); padding: 6px 14px; border-radius: 8px; font-family: 'Lora', serif; font-size: 13px; color: var(--text-muted); cursor: pointer; transition: all 0.2s; }
|
||||
.btn-history:hover { border-color: var(--accent); color: var(--text); }
|
||||
|
||||
.comment-popup { position: fixed; z-index: 300; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 14px 18px; box-shadow: 0 4px 20px var(--shadow-hover); max-width: 320px; min-width: 200px; }
|
||||
.comment-popup-author { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
|
||||
.comment-popup-author.e { color: var(--olive); }
|
||||
@@ -668,8 +688,11 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
||||
<div class="editor-empty" id="editor-empty">Choisir une scène dans le Binder</div>
|
||||
<div id="editor-active" style="display:none; flex:1; display:none; flex-direction:column;">
|
||||
<div class="editor-header">
|
||||
<div class="editor-scene-title" id="editor-title"></div>
|
||||
<div class="editor-scene-meta" id="editor-meta"></div>
|
||||
<div style="flex:1;">
|
||||
<div class="editor-scene-title" id="editor-title"></div>
|
||||
<div class="editor-scene-meta" id="editor-meta"></div>
|
||||
</div>
|
||||
<button class="btn-history" id="btn-history-redaction" onclick="openVersionPanel('redaction', currentSceneId)" style="display:none;">Historique</button>
|
||||
</div>
|
||||
<div class="editor-fiche" id="editor-fiche">
|
||||
<div class="editor-fiche-toggle" onclick="toggleFiche()">
|
||||
@@ -793,6 +816,7 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-delete" id="btn-delete" onclick="deleteCard()" style="display:none;">Supprimer</button>
|
||||
<button class="btn-history" id="btn-history-frise" onclick="openVersionPanel('frise', editingId)" style="display:none;">Historique</button>
|
||||
<button class="btn-save" onclick="saveCard()">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1290,6 +1314,7 @@ function openNew() {
|
||||
document.getElementById('f-lien').value = '';
|
||||
document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected'));
|
||||
document.getElementById('btn-delete').style.display = 'none';
|
||||
document.getElementById('btn-history-frise').style.display = 'none';
|
||||
document.getElementById('card-comments-section').style.display = 'none';
|
||||
document.getElementById('card-comment-list').innerHTML = '';
|
||||
document.getElementById('card-comment-input').value = '';
|
||||
@@ -1317,6 +1342,7 @@ function openEdit(id) {
|
||||
});
|
||||
|
||||
document.getElementById('btn-delete').style.display = 'block';
|
||||
document.getElementById('btn-history-frise').style.display = '';
|
||||
|
||||
// Comments
|
||||
const comments = parseComments(r.Commentaires);
|
||||
@@ -2665,6 +2691,7 @@ async function openScene(scene) {
|
||||
if (scene.Tags) meta.push(scene.Tags);
|
||||
if (scene.Personnage) meta.push(scene.Personnage);
|
||||
document.getElementById('editor-meta').textContent = meta.join(' · ');
|
||||
document.getElementById('btn-history-redaction').style.display = '';
|
||||
|
||||
// Panneau fiche (intention)
|
||||
buildFichePanel(scene);
|
||||
@@ -3030,6 +3057,146 @@ document.addEventListener('click', (e) => {
|
||||
closeCommentPopup();
|
||||
}
|
||||
});
|
||||
// ── Version history ──
|
||||
let versionType = null;
|
||||
let versionRecordId = null;
|
||||
let versionList = [];
|
||||
let selectedVersionId = null;
|
||||
|
||||
async function openVersionPanel(type, recordId) {
|
||||
if (!recordId) return;
|
||||
versionType = type;
|
||||
versionRecordId = recordId;
|
||||
selectedVersionId = null;
|
||||
document.getElementById('version-panel-title').textContent = type === 'frise' ? 'Historique — Fiche' : 'Historique — Texte';
|
||||
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/versions/${type}/${recordId}`);
|
||||
const data = await r.json();
|
||||
versionList = data.list || [];
|
||||
renderVersionList();
|
||||
} catch (e) {
|
||||
document.getElementById('version-list').innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center;">Erreur de chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function closeVersionPanel() {
|
||||
document.getElementById('version-panel').classList.remove('open');
|
||||
versionType = null;
|
||||
versionRecordId = null;
|
||||
selectedVersionId = null;
|
||||
}
|
||||
|
||||
function renderVersionList() {
|
||||
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 version enregistrée pour le moment.<br>Les versions seront créées automatiquement à chaque modification.</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 preview = '';
|
||||
try {
|
||||
const snap = JSON.parse(v.Snapshot);
|
||||
if (versionType === 'frise') {
|
||||
preview = [snap.Title, snap['Résumé']].filter(Boolean).join(' — ');
|
||||
} else {
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = snap.Texte || '';
|
||||
preview = tmp.textContent.substring(0, 150);
|
||||
}
|
||||
} catch (e) { preview = ''; }
|
||||
const label = v.Label || '';
|
||||
div.innerHTML = `<div class="version-item-date">${dateStr}</div>` +
|
||||
(label ? `<div class="version-item-label">${label.replace(/</g, '<')}</div>` : '') +
|
||||
(preview ? `<div class="version-item-preview">${preview.replace(/</g, '<').substring(0, 200)}</div>` : '');
|
||||
div.onclick = () => selectVersion(v.Id);
|
||||
container.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function selectVersion(id) {
|
||||
selectedVersionId = id;
|
||||
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 previewPane = document.getElementById('version-preview');
|
||||
const content = document.getElementById('version-preview-content');
|
||||
if (versionType === 'frise') {
|
||||
const parts = [];
|
||||
if (snap.Title) parts.push(`<strong>${snap.Title.replace(/</g, '<')}</strong>`);
|
||||
if (snap.Acte) parts.push(`<em>${snap.Acte}</em>`);
|
||||
if (snap['Résumé']) parts.push(snap['Résumé'].replace(/</g, '<'));
|
||||
if (snap['Intérêt narratif']) parts.push('<br><strong>Intérêt :</strong> ' + snap['Intérêt narratif'].replace(/</g, '<'));
|
||||
if (snap.Tags) parts.push('<br><em>Tags : ' + snap.Tags.replace(/</g, '<') + '</em>');
|
||||
content.innerHTML = parts.join('<br>');
|
||||
} else {
|
||||
content.innerHTML = DOMPurify.sanitize(snap.Texte || '<em>Texte vide</em>');
|
||||
}
|
||||
previewPane.style.display = '';
|
||||
document.getElementById('version-actions').style.display = '';
|
||||
} catch (e) {
|
||||
document.getElementById('version-preview').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreVersion() {
|
||||
if (!selectedVersionId) return;
|
||||
if (!confirm('Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.')) return;
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Close popup on version panel click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const panel = document.getElementById('version-panel');
|
||||
if (panel.classList.contains('open') && !panel.contains(e.target) && !e.target.closest('.btn-history')) {
|
||||
closeVersionPanel();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Panneau Historique des versions -->
|
||||
<div class="version-panel" id="version-panel">
|
||||
<div class="version-panel-header">
|
||||
<h3 id="version-panel-title">Historique</h3>
|
||||
<button class="modal-close" onclick="closeVersionPanel()">×</button>
|
||||
</div>
|
||||
<div class="version-panel-list" id="version-list"></div>
|
||||
<div class="version-preview-pane" id="version-preview" style="display:none;">
|
||||
<h4>Aperçu</h4>
|
||||
<div class="version-preview-content" id="version-preview-content"></div>
|
||||
</div>
|
||||
<div class="version-actions" id="version-actions" style="display:none;">
|
||||
<button class="btn-save" onclick="restoreVersion()">Restaurer cette version</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user