From afc223d79ee3b9d50fcdb0c796f1f9819c481dcb Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 12 Sep 2026 19:57:21 +0200 Subject: [PATCH] fix: an article opens as the article, and Editorial can delete Opening on the high-yield view was a defensible default and a bad one in practice: an author who had written ten sections opened their own page, saw two, and reasonably concluded the rest had not saved. It also made every reader's first impression of a topic its revision card. The article now opens on the fullest reading it has, and High yield is a switch you throw. Delete is on the Editorial row as well as in the editor. Having it only in the editor meant opening an article in order to get rid of it. And the Library button is gone from the Editorial header: it is the Reading page, it is in the menu, and a third door beside the two controls that are this page's own job is a door in the way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/components/ArticleReader.jsx | 8 +++++- frontend/src/pages/ArticlesPage.test.jsx | 19 ++++++++------ frontend/src/pages/EditorialPage.jsx | 30 ++++++++++++++++++++++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ArticleReader.jsx b/frontend/src/components/ArticleReader.jsx index 246ae7c..01fc3a7 100644 --- a/frontend/src/components/ArticleReader.jsx +++ b/frontend/src/components/ArticleReader.jsx @@ -130,7 +130,13 @@ export default function ArticleReader({ const everySection = (article.sections || []).map( sec => ({ ...sec, variant: sec.variant || 'long' })) const present = VIEWS.filter(v => everySection.some(sec => sec.variant === v.key)) - const [view, setView] = useState(present[0]?.key || 'long') + // The article opens as the article. It used to open on the high-yield view, + // because that is first in the list of views — and an author who had written + // ten sections of prose opened their own page and saw a summary, reasonably + // concluding the rest had not been saved. High yield is something you ask + // for; the switch is lit when you have. + const fullest = present.find(v => v.key !== SUMMARY)?.key + const [view, setView] = useState(fullest || present[0]?.key || 'long') const allSections = everySection.filter(sec => sec.variant === view) // One section is not a contents page. Short and Clinical are usually a single diff --git a/frontend/src/pages/ArticlesPage.test.jsx b/frontend/src/pages/ArticlesPage.test.jsx index 8fa2df9..437ea10 100644 --- a/frontend/src/pages/ArticlesPage.test.jsx +++ b/frontend/src/pages/ArticlesPage.test.jsx @@ -271,20 +271,25 @@ describe('topic reading', () => { await screen.findByText('Introduction markdown') const toc = () => document.querySelector('.article-sections') - expect(within(toc()).getByRole('button', { name: 'Key points' })).toBeInTheDocument() - expect(within(toc()).queryByRole('button', { name: 'Pathophysiology' })).toBeNull() + // The article opens as the article. Opening on the summary made an author + // who had written ten sections see two and conclude the rest had not + // saved — and made every reader's first impression of a topic its + // revision card. + expect(within(toc()).getByRole('button', { name: 'Pathophysiology' })).toBeInTheDocument() + expect(within(toc()).queryByRole('button', { name: 'Key points' })).toBeNull() + // The label is the same in both states — it is lit or it is not. What it // must still say out loud is which of the two it is in, so a reader who // cannot see the lit state can tell why two thirds of the contents are // not there. const summary = () => screen.getByRole('button', { name: 'High yield' }) - expect(summary()).toHaveAttribute('aria-pressed', 'true') + expect(summary()).toHaveAttribute('aria-pressed', 'false') await userEvent.click(summary()) - expect(within(toc()).getByRole('button', { name: 'Pathophysiology' })).toBeInTheDocument() - expect(within(toc()).queryByRole('button', { name: 'Key points' })).toBeNull() - expect(screen.queryByText('Key body')).not.toBeInTheDocument() - expect(summary()).toHaveAttribute('aria-pressed', 'false') + expect(within(toc()).getByRole('button', { name: 'Key points' })).toBeInTheDocument() + expect(within(toc()).queryByRole('button', { name: 'Pathophysiology' })).toBeNull() + expect(screen.queryByText('Long body')).not.toBeInTheDocument() + expect(summary()).toHaveAttribute('aria-pressed', 'true') }) it('nests a sub-section under its parent and keeps references last', async () => { diff --git a/frontend/src/pages/EditorialPage.jsx b/frontend/src/pages/EditorialPage.jsx index 4c7b66d..f6fac6b 100644 --- a/frontend/src/pages/EditorialPage.jsx +++ b/frontend/src/pages/EditorialPage.jsx @@ -57,6 +57,18 @@ export default function EditorialPage() { useEffect(() => { load() }, [load]) + //: Deleting from the queue, which is where an editor is standing when they + //: decide an article should not exist. The editor page has the same control; + //: having it only there meant opening an article to get rid of it. + const [removing, setRemoving] = useState(null) + + const remove = async (article) => { + setBusy(true); setError('') + try { await api.delete(`/articles/${article.id}`); setRemoving(null); load() } + catch (err) { setError(apiError(err, 'Could not delete that article')) } + finally { setBusy(false) } + } + const restore = async (article) => { setBusy(true); setError('') try { await api.post(`/articles/${article.id}/restore`); load() } @@ -96,7 +108,9 @@ export default function EditorialPage() { reachable from the library — a page about reading, behind a button an educator arriving here to work has no reason to look for. */}
- Library + {/* No Library button. It is the Reading page, it is in the menu, + and a second door to it beside the two controls that are this + page's own job is a door in the way. */} Write one Draft with AI
@@ -165,6 +179,20 @@ export default function EditorialPage() { aria-label={`Unpublish ${article.title}`} onClick={() => setStatus(article, 'draft')}>Unpublish )} + {removing === article.id ? ( + <> + + + + ) : ( + + )} ))}