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); }
|
.binder-scene.active .scene-words { color: rgba(255,255,255,0.6); }
|
||||||
|
|
||||||
.editor-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
.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-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-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; }
|
.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-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); }
|
.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 { 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 { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
|
||||||
.comment-popup-author.e { color: var(--olive); }
|
.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 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 id="editor-active" style="display:none; flex:1; display:none; flex-direction:column;">
|
||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
<div class="editor-scene-title" id="editor-title"></div>
|
<div style="flex:1;">
|
||||||
<div class="editor-scene-meta" id="editor-meta"></div>
|
<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>
|
||||||
<div class="editor-fiche" id="editor-fiche">
|
<div class="editor-fiche" id="editor-fiche">
|
||||||
<div class="editor-fiche-toggle" onclick="toggleFiche()">
|
<div class="editor-fiche-toggle" onclick="toggleFiche()">
|
||||||
@@ -793,6 +816,7 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-delete" id="btn-delete" onclick="deleteCard()" style="display:none;">Supprimer</button>
|
<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>
|
<button class="btn-save" onclick="saveCard()">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1290,6 +1314,7 @@ function openNew() {
|
|||||||
document.getElementById('f-lien').value = '';
|
document.getElementById('f-lien').value = '';
|
||||||
document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected'));
|
document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected'));
|
||||||
document.getElementById('btn-delete').style.display = 'none';
|
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-comments-section').style.display = 'none';
|
||||||
document.getElementById('card-comment-list').innerHTML = '';
|
document.getElementById('card-comment-list').innerHTML = '';
|
||||||
document.getElementById('card-comment-input').value = '';
|
document.getElementById('card-comment-input').value = '';
|
||||||
@@ -1317,6 +1342,7 @@ function openEdit(id) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-delete').style.display = 'block';
|
document.getElementById('btn-delete').style.display = 'block';
|
||||||
|
document.getElementById('btn-history-frise').style.display = '';
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
const comments = parseComments(r.Commentaires);
|
const comments = parseComments(r.Commentaires);
|
||||||
@@ -2665,6 +2691,7 @@ async function openScene(scene) {
|
|||||||
if (scene.Tags) meta.push(scene.Tags);
|
if (scene.Tags) meta.push(scene.Tags);
|
||||||
if (scene.Personnage) meta.push(scene.Personnage);
|
if (scene.Personnage) meta.push(scene.Personnage);
|
||||||
document.getElementById('editor-meta').textContent = meta.join(' · ');
|
document.getElementById('editor-meta').textContent = meta.join(' · ');
|
||||||
|
document.getElementById('btn-history-redaction').style.display = '';
|
||||||
|
|
||||||
// Panneau fiche (intention)
|
// Panneau fiche (intention)
|
||||||
buildFichePanel(scene);
|
buildFichePanel(scene);
|
||||||
@@ -3030,6 +3057,146 @@ document.addEventListener('click', (e) => {
|
|||||||
closeCommentPopup();
|
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>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -18,6 +18,27 @@ const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || '';
|
|||||||
const NOCODB_CONTENU_TABLE_ID = process.env.NOCODB_CONTENU_TABLE_ID || '';
|
const NOCODB_CONTENU_TABLE_ID = process.env.NOCODB_CONTENU_TABLE_ID || '';
|
||||||
const NOCODB_PERSONNAGES_TABLE_ID = process.env.NOCODB_PERSONNAGES_TABLE_ID || '';
|
const NOCODB_PERSONNAGES_TABLE_ID = process.env.NOCODB_PERSONNAGES_TABLE_ID || '';
|
||||||
const NOCODB_RESSOURCES_TABLE_ID = process.env.NOCODB_RESSOURCES_TABLE_ID || '';
|
const NOCODB_RESSOURCES_TABLE_ID = process.env.NOCODB_RESSOURCES_TABLE_ID || '';
|
||||||
|
const NOCODB_VERSIONS_TABLE_ID = process.env.NOCODB_VERSIONS_TABLE_ID || '';
|
||||||
|
|
||||||
|
// ── Version history helper ──
|
||||||
|
async function saveVersion(type, recordId, snapshot, label) {
|
||||||
|
if (!NOCODB_VERSIONS_TABLE_ID) return;
|
||||||
|
try {
|
||||||
|
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}`;
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
Type: type,
|
||||||
|
RecordId: recordId,
|
||||||
|
Snapshot: JSON.stringify(snapshot),
|
||||||
|
Label: label || '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('saveVersion error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
function makeToken(ts) {
|
function makeToken(ts) {
|
||||||
@@ -77,10 +98,19 @@ app.post('/api/records', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy NocoDB — update record
|
// Proxy NocoDB — update record (with version snapshot)
|
||||||
app.patch('/api/records/:id', requireAuth, async (req, res) => {
|
app.patch('/api/records/:id', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`;
|
const recordId = Number(req.params.id);
|
||||||
|
// Snapshot current state before update
|
||||||
|
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||||
|
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||||
|
if (getR.ok) {
|
||||||
|
const current = await getR.json();
|
||||||
|
await saveVersion('frise', recordId, current, current.Titre || '');
|
||||||
|
}
|
||||||
|
// Proceed with update
|
||||||
|
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||||
@@ -93,10 +123,18 @@ app.patch('/api/records/:id', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy NocoDB — delete record
|
// Proxy NocoDB — delete record (archive before deletion)
|
||||||
app.delete('/api/records/:id', requireAuth, async (req, res) => {
|
app.delete('/api/records/:id', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`;
|
const recordId = Number(req.params.id);
|
||||||
|
// Archive before delete
|
||||||
|
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||||
|
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||||
|
if (getR.ok) {
|
||||||
|
const current = await getR.json();
|
||||||
|
await saveVersion('frise', recordId, current, `[supprimé] ${current.Titre || ''}`);
|
||||||
|
}
|
||||||
|
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'xc-token': NOCODB_TOKEN },
|
headers: { 'xc-token': NOCODB_TOKEN },
|
||||||
@@ -243,6 +281,8 @@ app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
|||||||
const payload = { SceneId: sceneId, Texte: req.body.Texte, Mots: req.body.Mots || 0 };
|
const payload = { SceneId: sceneId, Texte: req.body.Texte, Mots: req.body.Mots || 0 };
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
// Snapshot before update
|
||||||
|
await saveVersion('redaction', sceneId, { Texte: existing.Texte, Mots: existing.Mots, SceneId: sceneId }, `Scène #${sceneId}`);
|
||||||
// Update
|
// Update
|
||||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
@@ -268,6 +308,83 @@ app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Versions API ──
|
||||||
|
|
||||||
|
// List versions for a record
|
||||||
|
app.get('/api/versions/:type/:recordId', requireAuth, async (req, res) => {
|
||||||
|
if (!NOCODB_VERSIONS_TABLE_ID) return res.json({ list: [] });
|
||||||
|
try {
|
||||||
|
const { type, recordId } = req.params;
|
||||||
|
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}?where=(Type,eq,${type})~and(RecordId,eq,${recordId})&sort=-CreatedAt&limit=50`;
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore a frise version
|
||||||
|
app.post('/api/versions/:id/restore', requireAuth, async (req, res) => {
|
||||||
|
if (!NOCODB_VERSIONS_TABLE_ID) return res.status(400).json({ error: 'Versions non configurées' });
|
||||||
|
try {
|
||||||
|
// Get the version
|
||||||
|
const vUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}/${req.params.id}`;
|
||||||
|
const vR = await fetch(vUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||||
|
if (!vR.ok) return res.status(404).json({ error: 'Version introuvable' });
|
||||||
|
const version = await vR.json();
|
||||||
|
const snapshot = JSON.parse(version.Snapshot);
|
||||||
|
|
||||||
|
if (version.Type === 'frise') {
|
||||||
|
// Snapshot current before restoring
|
||||||
|
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${version.RecordId}`;
|
||||||
|
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||||
|
if (getR.ok) {
|
||||||
|
const current = await getR.json();
|
||||||
|
await saveVersion('frise', version.RecordId, current, `[avant restauration] ${current.Titre || ''}`);
|
||||||
|
}
|
||||||
|
// Restore — only user-editable fields
|
||||||
|
const { Titre, Resume, Personnage, Tags, Notes, Acte, Ordre } = snapshot;
|
||||||
|
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${version.RecordId}`;
|
||||||
|
const r = await fetch(patchUrl, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ Titre, Resume, Personnage, Tags, Notes, Acte, Ordre }),
|
||||||
|
});
|
||||||
|
res.json(await r.json());
|
||||||
|
} else if (version.Type === 'redaction') {
|
||||||
|
// Snapshot current text before restoring
|
||||||
|
const findUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}?where=(SceneId,eq,${version.RecordId})&limit=1`;
|
||||||
|
const findR = await fetch(findUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||||
|
const findData = await findR.json();
|
||||||
|
const existing = findData.list?.[0];
|
||||||
|
if (existing) {
|
||||||
|
await saveVersion('redaction', version.RecordId, { Texte: existing.Texte, Mots: existing.Mots, SceneId: version.RecordId }, `[avant restauration] Scène #${version.RecordId}`);
|
||||||
|
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
||||||
|
const r = await fetch(patchUrl, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ Texte: snapshot.Texte, Mots: snapshot.Mots || 0 }),
|
||||||
|
});
|
||||||
|
res.json(await r.json());
|
||||||
|
} else {
|
||||||
|
// Re-create content row
|
||||||
|
const createUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}`;
|
||||||
|
const r = await fetch(createUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ SceneId: version.RecordId, Texte: snapshot.Texte, Mots: snapshot.Mots || 0 }),
|
||||||
|
});
|
||||||
|
res.json(await r.json());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: 'Type inconnu' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Personnages API ──
|
// ── Personnages API ──
|
||||||
|
|
||||||
function personnagesProxy(tableIdVar) {
|
function personnagesProxy(tableIdVar) {
|
||||||
|
|||||||
Reference in New Issue
Block a user