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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
5670ebbdda
commit
afc223d79e
3 changed files with 48 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
<div className="ed-header-actions">
|
||||
<Link className="btn btn-secondary" to="/articles">Library</Link>
|
||||
{/* 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. */}
|
||||
<Link className="btn btn-secondary" to="/articles?new=1">Write one</Link>
|
||||
<Link className="btn btn-primary" to="/articles?draft=1">Draft with AI</Link>
|
||||
</div>
|
||||
|
|
@ -165,6 +179,20 @@ export default function EditorialPage() {
|
|||
aria-label={`Unpublish ${article.title}`}
|
||||
onClick={() => setStatus(article, 'draft')}>Unpublish</button>
|
||||
)}
|
||||
{removing === article.id ? (
|
||||
<>
|
||||
<button className="btn btn-danger btn-sm" disabled={busy}
|
||||
onClick={() => remove(article)}>
|
||||
{article.status === 'published' ? 'Move to trash' : 'Delete for good'}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm"
|
||||
onClick={() => setRemoving(null)}>Cancel</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-secondary btn-sm" disabled={busy}
|
||||
aria-label={`Delete ${article.title}`}
|
||||
onClick={() => setRemoving(article.id)}>Delete</button>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue