feat: refactorer ShaaritThemeConfig.save() pour utiliser readEditForm() unifié (GET /admin/shaare?post=<url>) qui retourne token/lfId/existing en une seule requête, éliminer readExistingConfig() et getTokenFrom() redondants, auto-détection create vs update via présence lf_id dans form Shaarli, cleanup duplicates en excluant bookmark sauvegardé (lfId ou premier candidat), et retourner flag created dans résultat save

This commit is contained in:
Bruno Charest 2026-04-20 21:02:59 -04:00
parent f90f8146ce
commit aaf8e902a1

View File

@ -42,28 +42,30 @@
return [...byId.values()].sort((a, b) => Number(a.id) - Number(b.id)); return [...byId.values()].sort((a, b) => Number(a.id) - Number(b.id));
} }
// Reads existing config JSON from the edit page's textarea (raw value, // Reads the shared edit page (GET /admin/shaare?post=<url>):
// not affected by the markdown plugin). // - Shaarli returns the editlink form pre-filled with the existing
async function readExistingConfig(editUrl) { // bookmark (lf_id populated) when the URL already exists, or a blank
try { // edit form (no lf_id) when creating new.
const doc = await fetchDoc(editUrl); // Returns { token, lfId, existing } where existing is the parsed JSON
const ta = doc.querySelector('textarea[name="lf_description"]'); // description (or null).
if (!ta) return null; async function readEditForm(postUrl) {
const raw = (ta.textContent || '').trim(); const basePath = getBasePath();
if (!raw) return null; const src = basePath + '/admin/shaare?post=' + encodeURIComponent(postUrl);
return JSON.parse(raw); const doc = await fetchDoc(src);
} catch (e) {
return null;
}
}
async function getTokenFrom(url) {
const doc = await fetchDoc(url);
const tokenEl = doc.querySelector('input[name="token"]'); const tokenEl = doc.querySelector('input[name="token"]');
const idEl = doc.querySelector('input[name="lf_id"]'); const idEl = doc.querySelector('input[name="lf_id"]');
const ta = doc.querySelector('textarea[name="lf_description"]');
let existing = null;
if (ta) {
const raw = (ta.textContent || '').trim();
if (raw) {
try { existing = JSON.parse(raw); } catch (e) { /* non-JSON */ }
}
}
return { return {
token: tokenEl ? tokenEl.value : '', token: tokenEl ? tokenEl.value : '',
lfId: idEl ? idEl.value : '', lfId: idEl ? idEl.value : '',
existing,
}; };
} }
@ -77,18 +79,27 @@
} }
// Merge partial updates into the persisted config and save to the single // Merge partial updates into the persisted config and save to the single
// bookmark. Deletes any duplicates left over from previous buggy saves. // bookmark. Creates one when none exists, updates the existing one, and
// deletes any duplicates left over from previous buggy saves.
async function save(partial) { async function save(partial) {
// Serialize so rapid toggles do not race and create duplicates. // Serialize so rapid toggles do not race and create duplicates.
const run = async () => { const run = async () => {
const basePath = getBasePath(); const basePath = getBasePath();
const candidates = await findCandidates();
const primary = candidates[0] || null;
const duplicates = candidates.slice(1);
// Build merged config // Collect duplicates via tag search (best-effort)
let existing = null; const candidates = await findCandidates();
if (primary) existing = await readExistingConfig(primary.editUrl);
// Always read the edit form for CONFIG_URL: Shaarli returns the
// existing bookmark pre-filled (with lf_id) when the URL exists,
// or a blank edit form (no lf_id) otherwise. This path works for
// both create and update and self-heals if search missed it.
const { token, lfId, existing } = await readEditForm(CONFIG_URL);
if (!token) {
console.warn('[shaarit] theme-config: no CSRF token from edit form');
return { ok: false, reason: 'no-token' };
}
// Merge config (preserve any existing fields not in partial)
const base = existing && typeof existing === 'object' ? existing : {}; const base = existing && typeof existing === 'object' ? existing : {};
const config = Object.assign( const config = Object.assign(
{ version: 2, themes: [], default: 'DEFAULT', mode: 'dark' }, { version: 2, themes: [], default: 'DEFAULT', mode: 'dark' },
@ -96,11 +107,6 @@
partial || {} partial || {}
); );
// Get token + lf_id from the primary's edit page (or add page)
const src = primary ? primary.editUrl : basePath + '/admin/add-shaare';
const { token, lfId } = await getTokenFrom(src);
if (!token) return { ok: false, reason: 'no-token' };
const fd = new FormData(); const fd = new FormData();
fd.append('lf_title', CONFIG_TITLE); fd.append('lf_title', CONFIG_TITLE);
fd.append('lf_url', CONFIG_URL); fd.append('lf_url', CONFIG_URL);
@ -117,13 +123,15 @@
credentials: 'same-origin', credentials: 'same-origin',
}); });
// Clean up any leftover duplicates (reuse the same token, Shaarli // Delete any duplicates left over from previous buggy saves,
// accepts the session token for both edit and delete). // keeping the one we just saved (lfId) or the first candidate.
for (const dup of duplicates) { const keepId = lfId || (candidates[0] && candidates[0].id) || null;
await deleteBookmark(dup.id, token); for (const c of candidates) {
if (keepId && String(c.id) === String(keepId)) continue;
await deleteBookmark(c.id, token);
} }
return { ok: true, id: lfId || (primary && primary.id) || null, config }; return { ok: true, id: keepId, config, created: !lfId };
}; };
const p = (inflight ? inflight.catch(() => {}) : Promise.resolve()).then(run); const p = (inflight ? inflight.catch(() => {}) : Promise.resolve()).then(run);