fixes: collab move, caldav abort, draft recovery, autobackup, test stabilization
- Collab: moving a shared card to a local bucket no longer deletes it (clear relay_uid before tombstone echo returns) - CalDAV: abort sync on remote fetch failure instead of treating all local cards as remotely-deleted - Editor: autobackup drafts to localStorage with restore prompt on re-open (skipped during tests) - Cards POST: persist scratchpad and due_date (was dropped on create) - Disable spellcheck/autocorrect on editor inputs - Test runner: purge leftover __prefix__ cards/buckets + editor drafts before every run (cleans up from failed previous runs) - Stabilize mark-done, group-delete, undo-body tests with poll loops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7e2b130434
commit
3cca14a3c6
8 files changed
+178
-23
No files matched your search
@@ -13,6 +13,8 @@ const inpTitle = $('inp-title');
|
||||
const inpColor = $('inp-color');
|
||||
const inpBucket = $('inp-bucket');
|
||||
const presets = $('color-presets');
|
||||
const scratchpadPanel = $('editor-scratchpad');
|
||||
const scratchpadBody = $('scratchpad-body');
|
||||
const linkAc = $('link-ac');
|
||||
const tagAc = $('tag-ac');
|
||||
|
||||
@@ -176,6 +178,45 @@ function selectColor(hex) {
|
||||
|
||||
// ── Open / close ──
|
||||
|
||||
// ── Autobackup (draft recovery) ──
|
||||
const DRAFT_KEY = 'hyprcrd-editor-draft';
|
||||
let draftSaveTimer = null;
|
||||
|
||||
function draftKeyFor(cardId) {
|
||||
return DRAFT_KEY + ':' + (cardId || 'new');
|
||||
}
|
||||
|
||||
function loadDraft(cardId) {
|
||||
try {
|
||||
const raw = localStorage.getItem(draftKeyFor(cardId));
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function saveDraftDebounced() {
|
||||
clearTimeout(draftSaveTimer);
|
||||
draftSaveTimer = setTimeout(() => {
|
||||
const id = state.editingId || 'new';
|
||||
const data = {
|
||||
title: inpTitle.value,
|
||||
body: body.value,
|
||||
scratchpad: scratchpadBody?.value || '',
|
||||
savedAt: Date.now(),
|
||||
};
|
||||
// Don't store empty drafts
|
||||
if (!data.title.trim() && !data.body.trim() && !data.scratchpad.trim()) {
|
||||
localStorage.removeItem(draftKeyFor(id));
|
||||
return;
|
||||
}
|
||||
try { localStorage.setItem(draftKeyFor(id), JSON.stringify(data)); } catch {}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function clearDraft(cardId) {
|
||||
clearTimeout(draftSaveTimer);
|
||||
try { localStorage.removeItem(draftKeyFor(cardId || 'new')); } catch {}
|
||||
}
|
||||
|
||||
export function openModal(card = null) {
|
||||
state.editingId = card ? card.id : null;
|
||||
$('modal-title').textContent = card ? 'edit_card' : 'new_card';
|
||||
@@ -195,15 +236,44 @@ export function openModal(card = null) {
|
||||
if (linesVisible) updateGutter();
|
||||
currentLine = -1;
|
||||
updateActiveLine();
|
||||
|
||||
// Check for unsaved draft (newer than what we just loaded).
|
||||
// Skipped during test runs — tests would block on the confirm dialog.
|
||||
const draft = loadDraft(card?.id);
|
||||
if (draft && !window._testRunning) {
|
||||
const cardModified = card ? Date.parse(card.updated_at + 'Z') || 0 : 0;
|
||||
const draftIsNewer = draft.savedAt > cardModified;
|
||||
const draftHasContent = draft.title || draft.body || draft.scratchpad;
|
||||
const differsFromLoaded = draft.title !== inpTitle.value || draft.body !== body.value || draft.scratchpad !== (scratchpadBody?.value || '');
|
||||
if (draftIsNewer && draftHasContent && differsFromLoaded) {
|
||||
const age = Math.round((Date.now() - draft.savedAt) / 60000);
|
||||
const ageText = age < 1 ? 'just now' : age < 60 ? `${age} min ago` : `${Math.round(age / 60)} h ago`;
|
||||
const restore = window.confirm(`Unsaved draft from ${ageText} found.\n\nRestore it?`);
|
||||
if (restore) {
|
||||
inpTitle.value = draft.title || '';
|
||||
body.value = draft.body || '';
|
||||
if (scratchpadBody) scratchpadBody.value = draft.scratchpad || '';
|
||||
updatePreview();
|
||||
} else {
|
||||
clearDraft(card?.id);
|
||||
}
|
||||
}
|
||||
} else if (draft && window._testRunning) {
|
||||
// During tests, silently discard any draft so it doesn't accumulate
|
||||
clearDraft(card?.id);
|
||||
}
|
||||
|
||||
inpTitle.focus();
|
||||
// Plugins (collab-buckets) hook into this to heartbeat an edit lock
|
||||
window.dispatchEvent(new CustomEvent('hyprcrd:editor-open', { detail: { card } }));
|
||||
}
|
||||
|
||||
inpTitle.addEventListener('input', updatePreview);
|
||||
inpTitle.addEventListener('input', () => { updatePreview(); saveDraftDebounced(); });
|
||||
inpTitle.addEventListener('keydown', e => {
|
||||
if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); body.focus(); }
|
||||
});
|
||||
body.addEventListener('input', saveDraftDebounced);
|
||||
scratchpadBody?.addEventListener('input', saveDraftDebounced);
|
||||
|
||||
// ── Line numbers ──
|
||||
|
||||
@@ -396,10 +466,12 @@ async function doSave(close, { animate = true } = {}) {
|
||||
const data = { title: inpTitle.value, body: body.value, color: inpColor.value, bucket_id: inpBucket.value || null, due_date: inpDueValue?.value || null, scratchpad: scratch };
|
||||
if (!data.title.trim()) return;
|
||||
const returnToGallery = state.returnToGalleryAfterEdit;
|
||||
const wasEditingId = state.editingId;
|
||||
if (state.editingId) {
|
||||
const prev = snapshotCard(state.cards.find(c => c.id == state.editingId));
|
||||
if (prev) pushCardEdit('edit', state.editingId, { title: prev.title, body: prev.body, color: prev.color, bucket_id: prev.bucket_id, due_date: prev.due_date ?? null });
|
||||
await api(`api/cards/${state.editingId}`, { method: 'PATCH', body: data });
|
||||
clearDraft(state.editingId);
|
||||
} else {
|
||||
if (state.activeGroup) {
|
||||
const parent = state.cards.find(c => c.id == state.activeGroup);
|
||||
@@ -416,6 +488,7 @@ async function doSave(close, { animate = true } = {}) {
|
||||
state._lastSavedId = created.id;
|
||||
state.recentlyCreatedIds?.add(String(created.id));
|
||||
}
|
||||
clearDraft(null); // 'new' draft
|
||||
}
|
||||
await window._reloadAll();
|
||||
// Auto-sync if card has #termin or #geburtstag
|
||||
@@ -533,9 +606,6 @@ function applyEditorSplit() {
|
||||
preview.style.flex = `${100 - editorSplit} 1 0`;
|
||||
}
|
||||
|
||||
const scratchpadPanel = $('editor-scratchpad');
|
||||
const scratchpadBody = $('scratchpad-body');
|
||||
|
||||
function showSidePanel(which) {
|
||||
const showPreview = which === 'preview';
|
||||
const showScratch = which === 'scratchpad';
|
||||
|
||||
Reference in new issue
Block a user