Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions frontend/src/pages/groups/GroupTextFileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ export default function GroupTextFileEditor({
retry: false,
});

// A fresh load only replaces the textarea when the user has no unsaved edits in it.
useEffect(() => {
if (data && !dirty) setDraft(data.content);
}, [data, dirty]);

// Clear edits for the previous query before hydrating this editor. This effect must stay before
// the data effect: useQuery may synchronously return cached content when the tab remounts.
useEffect(() => {
setDraft('');
setDirty(false);
}, [JSON.stringify(queryKey)]);

// A fresh load only replaces the textarea when the user has no unsaved edits in it.
useEffect(() => {
if (data && !dirty) setDraft(data.content);
}, [data, dirty]);

const commit = async () => {
setBusy(true);
try {
Expand Down
36 changes: 36 additions & 0 deletions frontend/tests/groupAnnouncementEditor.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

const editor = readFileSync(
new URL('../src/pages/groups/GroupTextFileEditor.tsx', import.meta.url),
'utf8',
);

function draftAfterCachedMount(source, cachedContent) {
const effectStart = source.indexOf('const { data, isLoading, error, refetch }');
const effectEnd = source.indexOf('const commit = async');
const effects = source.slice(effectStart, effectEnd);
const hydrateIndex = effects.indexOf('if (data && !dirty) setDraft(data.content);');
const resetIndex = effects.indexOf("setDraft('');");

assert.notEqual(hydrateIndex, -1, 'cached-data hydration effect is missing');
assert.notEqual(resetIndex, -1, 'query-key reset effect is missing');

let draft = '';
const mountUpdates = [
{ index: hydrateIndex, apply: () => { draft = cachedContent; } },
{ index: resetIndex, apply: () => { draft = ''; } },
].sort((left, right) => left.index - right.index);

for (const update of mountUpdates) update.apply();
return draft;
}

test('cached group announcement remains visible when its tab remounts', () => {
const cachedContent = 'cached group announcement';

// React runs mount effects in declaration order. A structurally equal refetch keeps the cached
// data reference, so this mount sequence is the only chance to hydrate the controlled textarea.
assert.equal(draftAfterCachedMount(editor, cachedContent), cachedContent);
});