fix: serialize article autosaves
This commit is contained in:
@@ -23,7 +23,9 @@
|
||||
let coverUploading = $state(false);
|
||||
let initialized = $state(false);
|
||||
let lastContent = '';
|
||||
let saveSequence = 0;
|
||||
let saveTimer: number | undefined;
|
||||
let saveInFlight = false;
|
||||
let saveQueued = false;
|
||||
const groupedTags = $derived.by(() => {
|
||||
const groups: Record<string, ComposerTag[]> = {};
|
||||
for (const tag of data.tags as ComposerTag[]) (groups[tag.group] ??= []).push(tag);
|
||||
@@ -35,6 +37,40 @@
|
||||
coverImageId, coverAlt, coverCaption, version };
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
if (saveTimer !== undefined) window.clearTimeout(saveTimer);
|
||||
saveTimer = window.setTimeout(() => {
|
||||
saveTimer = undefined;
|
||||
void saveDraft();
|
||||
}, 900);
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
if (saveInFlight || !saveQueued) return;
|
||||
saveInFlight = true;
|
||||
saveQueued = false;
|
||||
const draft = { ...payload(), tagIds: [...selectedTags] };
|
||||
try {
|
||||
const response = await fetch('/api/articles/draft', {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(draft)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.status === 409) {
|
||||
saveState = 'conflict';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error(result.message || 'Draft save failed.');
|
||||
version = result.version;
|
||||
saveState = saveQueued ? 'saving' : 'saved';
|
||||
} catch (error) {
|
||||
saveState = 'error';
|
||||
toast.error('Draft not saved', error instanceof Error ? error.message : 'Try again.');
|
||||
} finally {
|
||||
saveInFlight = false;
|
||||
if (saveQueued && saveState !== 'conflict') scheduleSave();
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const content = JSON.stringify({ title, deck, document, selectedTags, channel, coverImageId, coverAlt, coverCaption });
|
||||
if (!initialized) {
|
||||
@@ -44,29 +80,13 @@
|
||||
}
|
||||
if (content === lastContent) return;
|
||||
lastContent = content;
|
||||
const sequence = ++saveSequence;
|
||||
saveQueued = true;
|
||||
saveState = 'saving';
|
||||
const timer = window.setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/articles/draft', {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload())
|
||||
});
|
||||
const result = await response.json();
|
||||
if (sequence !== saveSequence) return;
|
||||
if (response.status === 409) {
|
||||
saveState = 'conflict';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) throw new Error(result.message || 'Draft save failed.');
|
||||
version = result.version;
|
||||
saveState = 'saved';
|
||||
} catch (error) {
|
||||
if (sequence !== saveSequence) return;
|
||||
saveState = 'error';
|
||||
toast.error('Draft not saved', error instanceof Error ? error.message : 'Try again.');
|
||||
}
|
||||
}, 900);
|
||||
return () => window.clearTimeout(timer);
|
||||
if (!saveInFlight) scheduleSave();
|
||||
return () => {
|
||||
if (saveTimer !== undefined) window.clearTimeout(saveTimer);
|
||||
saveTimer = undefined;
|
||||
};
|
||||
});
|
||||
|
||||
function toggleTag(id: string) {
|
||||
@@ -115,7 +135,7 @@
|
||||
<div class="article-publication-note"><p>{visibility === 'private' ? 'Only you can open this draft.' : visibility === 'unlisted' ? 'Anyone with the direct URL can read it.' : 'Listed in discovery and visible to everyone.'}</p>{#if data.article.pendingRevisionId}<strong>MODERATION REVIEW PENDING</strong>{/if}</div>
|
||||
<PendingForm action="?/publish" class="article-publish-form">
|
||||
<input type="hidden" name="articleId" value={data.article.id}/><input type="hidden" name="title" value={title}/><input type="hidden" name="deck" value={deck}/><input type="hidden" name="document" value={document}/><input type="hidden" name="tagIds" value={JSON.stringify(selectedTags)}/><input type="hidden" name="channel" value={channel}/><input type="hidden" name="coverImageId" value={coverImageId}/><input type="hidden" name="coverAlt" value={coverAlt}/><input type="hidden" name="coverCaption" value={coverCaption}/><input type="hidden" name="version" value={version}/><input type="hidden" name="visibility" value={visibility}/>
|
||||
<button class="primary">{visibility === 'private' ? 'SAVE AS PRIVATE' : data.article.kind === 'guide' ? 'SUBMIT FOR PUBLICATION' : 'PUBLISH ARTICLE'}</button>
|
||||
<button class="primary" disabled={saveState === 'saving' || coverUploading}>{visibility === 'private' ? 'SAVE AS PRIVATE' : data.article.kind === 'guide' ? 'SUBMIT FOR PUBLICATION' : 'PUBLISH ARTICLE'}</button>
|
||||
</PendingForm>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user