diff --git a/frontend/src/components/FacetPicker.jsx b/frontend/src/components/FacetPicker.jsx index 2eb25f0..394247b 100644 --- a/frontend/src/components/FacetPicker.jsx +++ b/frontend/src/components/FacetPicker.jsx @@ -24,7 +24,14 @@ export function FacetRow({ label, summary, extra, chip, onOpen }) { * `children` is called with the current search query so each facet decides how * to filter its own options. */ -export default function FacetPicker({ title, helper, open, onClose, onReset, searchLabel, children }) { +export default function FacetPicker({ + title, helper, open, onClose, onReset, searchLabel, children, + // What is currently picked, as {id, label}. Shown as chips you can take off + // one at a time: without them, unpicking something means finding its checkbox + // again in a list of several hundred, and Reset is the only alternative — + // which throws away the other nine choices to undo the tenth. + selected = [], onRemove, +}) { const [query, setQuery] = useState('') useEffect(() => { @@ -50,6 +57,23 @@ export default function FacetPicker({ title, helper, open, onClose, onReset, sea placeholder="Search" aria-label={searchLabel || `Search ${title}`} /> + {selected.length > 0 && onRemove && ( +
+ {selected.map(item => ( + + ))} + {selected.length > 1 && onReset && ( + + )} +
+ )} + {helper &&

{helper}

}

Include questions from:

diff --git a/frontend/src/pages/CustomQuizPage.css b/frontend/src/pages/CustomQuizPage.css index 925742f..c4329e0 100644 --- a/frontend/src/pages/CustomQuizPage.css +++ b/frontend/src/pages/CustomQuizPage.css @@ -267,3 +267,28 @@ .custom-test-refresh { flex: 0 0 auto; } .custom-test-start { flex: 1; width: auto; margin-left: 0; min-width: 0; } } + +/* What you have picked, inside the picker. Taking one off should not mean + hunting its checkbox back down in a list of several hundred, and Reset is no + answer — it throws away nine choices to undo the tenth. */ +.facet-panel-chosen { + display: flex; flex-wrap: wrap; gap: 6px; + padding: 10px 18px 0; +} +.facet-chosen-chip { + display: inline-flex; align-items: center; gap: 7px; + min-height: 28px; padding: 4px 9px; cursor: pointer; + border: 1px solid var(--option-sel-bd, var(--primary)); border-radius: 999px; + background: var(--option-sel-bg); color: var(--primary); + font: inherit; font-size: 0.78rem; font-weight: 600; +} +.facet-chosen-chip:hover { background: var(--primary); color: #fff; } +.facet-chosen-chip span:first-child { + max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.facet-chosen-clear { + min-height: 28px; padding: 4px 9px; cursor: pointer; + background: none; border: 0; font: inherit; font-size: 0.76rem; + color: var(--text-muted); text-decoration: underline; +} +.facet-chosen-clear:hover { color: var(--wrong-fg); } diff --git a/frontend/src/pages/CustomQuizPage.jsx b/frontend/src/pages/CustomQuizPage.jsx index 6d43f72..e6b39cb 100644 --- a/frontend/src/pages/CustomQuizPage.jsx +++ b/frontend/src/pages/CustomQuizPage.jsx @@ -377,6 +377,8 @@ export default function CustomQuizPage() { {/* ── Facet pickers ────────────────────────────────────────── */} setOpenFacet(null)} onReset={() => setTagIds(ids => ids.filter(id => !systemTags.some(t => t.id === id)))} + selected={systemTags.filter(t => tagIds.includes(t.id)).map(t => ({ id: t.id, label: t.name }))} + onRemove={id => toggleTag(id, false)} helper="Body systems. Flat by nature — there are only so many."> {query => { const shown = systemTags.filter(t => !query || t.name.toLowerCase().includes(query)) @@ -394,6 +396,8 @@ export default function CustomQuizPage() { setOpenFacet(null)} onReset={() => setCategoryIds([])} + selected={categories.filter(c => categoryIds.includes(c.id)).map(c => ({ id: c.id, label: c.name }))} + onRemove={id => toggleCategory(id, false)} helper="Open a system to see what is under it. Choosing one includes everything beneath."> {query => ( setOpenFacet(null)} onReset={() => setTagIds(ids => ids.filter(id => !subjectTags.some(t => t.id === id)))} + selected={subjectTags.filter(t => tagIds.includes(t.id)).map(t => ({ id: t.id, label: t.name }))} + onRemove={id => toggleTag(id, false)} helper="By default, all disciplines are included unless filters are selected."> {query => checkList(subjectTags, t => tagIds.includes(t.id), (t, on) => toggleTag(t.id, on), query)} setOpenFacet(null)} onReset={() => setTagIds(ids => ids.filter(id => !keywordTags.some(t => t.id === id)))} + selected={keywordTags.filter(t => tagIds.includes(t.id)).map(t => ({ id: t.id, label: t.name }))} + onRemove={id => toggleTag(id, false)} helper="By default, all keywords are included unless filters are selected."> {query => checkList(keywordTags, t => tagIds.includes(t.id), (t, on) => toggleTag(t.id, on), query)} setOpenFacet(null)} onReset={() => setArticleIds([])} + selected={articles.filter(a => articleIds.includes(a.id)).map(a => ({ id: a.id, label: a.title }))} + onRemove={id => setArticleIds(ids => ids.filter(x => x !== id))} helper="Pick the reading whose linked questions you want."> {query => articles.length === 0 ?

No articles yet.

diff --git a/frontend/src/pages/CustomQuizPage.test.jsx b/frontend/src/pages/CustomQuizPage.test.jsx index 33a5bce..294c035 100644 --- a/frontend/src/pages/CustomQuizPage.test.jsx +++ b/frontend/src/pages/CustomQuizPage.test.jsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor , within} from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -202,3 +202,25 @@ describe('CustomQuizPage', () => { expect(screen.getByRole('button', { name: 'Create Test' })).toBeDisabled() }) }) + +it('shows what you picked inside the picker, and lets you take one off', async () => { + api.get.mockImplementation(url => { + if (url === '/question-categories/') return Promise.resolve({ data: categories }) + if (url.startsWith('/tags')) return Promise.resolve({ data: TAGS }) + if (url.startsWith('/articles') || url.startsWith('/collections')) return Promise.resolve({ data: [] }) + return Promise.resolve({ data: { count: 30 } }) + }) + renderBuilder() + await userEvent.click(await screen.findByRole('button', { name: /Disciplines/ })) + + const panel = screen.getByRole('dialog', { name: 'Disciplines' }) + await userEvent.click(within(panel).getByRole('checkbox', { name: /Cardiology/ })) + + // The chip is the way back out. Hunting the checkbox down again in a list of + // several hundred is not, and Reset throws away every other choice. + const chip = within(panel).getByRole('button', { name: 'Remove Cardiology' }) + await userEvent.click(chip) + + expect(within(panel).queryByRole('button', { name: 'Remove Cardiology' })).not.toBeInTheDocument() + expect(within(panel).getByRole('checkbox', { name: /Cardiology/ })).not.toBeChecked() +})