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));
}
// Reads existing config JSON from the edit page's textarea (raw value,
// not affected by the markdown plugin).
async function readExistingConfig(editUrl) {
try {
const doc = await fetchDoc(editUrl);
const ta = doc.querySelector('textarea[name="lf_description"]');
if (!ta) return null;
const raw = (ta.textContent || '').trim();
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
async function getTokenFrom(url) {
const doc = await fetchDoc(url);
// Reads the shared edit page (GET /admin/shaare?post=<url>):
// - Shaarli returns the editlink form pre-filled with the existing
// bookmark (lf_id populated) when the URL already exists, or a blank
// edit form (no lf_id) when creating new.
// Returns { token, lfId, existing } where existing is the parsed JSON
// description (or null).
async function readEditForm(postUrl) {
const basePath = getBasePath();
const src = basePath + '/admin/shaare?post=' + encodeURIComponent(postUrl);
const doc = await fetchDoc(src);
const tokenEl = doc.querySelector('input[name="token"]');
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 {
token: tokenEl ? tokenEl.value : '',
lfId: idEl ? idEl.value : '',
existing,
};
}
@ -77,18 +79,27 @@
}
// 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) {
// Serialize so rapid toggles do not race and create duplicates.
const run = async () => {
const basePath = getBasePath();
const candidates = await findCandidates();
const primary = candidates[0] || null;
const duplicates = candidates.slice(1);
// Build merged config
let existing = null;
if (primary) existing = await readExistingConfig(primary.editUrl);
// Collect duplicates via tag search (best-effort)
const candidates = await findCandidates();
// 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 config = Object.assign(
{ version: 2, themes: [], default: 'DEFAULT', mode: 'dark' },
@ -96,11 +107,6 @@
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();
fd.append('lf_title', CONFIG_TITLE);
fd.append('lf_url', CONFIG_URL);
@ -117,13 +123,15 @@
credentials: 'same-origin',
});
// Clean up any leftover duplicates (reuse the same token, Shaarli
// accepts the session token for both edit and delete).
for (const dup of duplicates) {
await deleteBookmark(dup.id, token);
// Delete any duplicates left over from previous buggy saves,
// keeping the one we just saved (lfId) or the first candidate.
const keepId = lfId || (candidates[0] && candidates[0].id) || null;
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);