Enhance seed library functionality by integrating scope rendering and improving filter interactions

This commit is contained in:
Brenda Wallace committed 2026-11-04 07:39:36 +00:00
1 parent bf6e973bf0
commit d5ef4329e2
8 files changed
+76 -12

No files matched your search

+18 -6
View File
@@ -57,7 +57,8 @@ function Choice({params, search, active, className, onChoose, children}) {
// again, which Rails renders (seeds/_tabs, seeds/_results) and this puts in
// place. Links in the tabs and results that stay on this page, such as the
// pages, do the same. Back and forward work.
export default function SeedLibrary({tabs: initialTabs, html: initialHtml, groups, filters, sow_now: sowNow, gardeners, months = [], clear = {}, crop: initialCrop, crop_search: cropSearch, placeholder}) {
export default function SeedLibrary({scope: initialScope, tabs: initialTabs, html: initialHtml, groups, filters, sow_now: sowNow, gardeners, months = [], clear = {}, crop: initialCrop, crop_search: cropSearch, placeholder}) {
const [scope, setScope] = useState(initialScope);
const [tabs, setTabs] = useState(initialTabs);
const [html, setHtml] = useState(initialHtml);
const [search, setSearch] = useState(window.location.search);
@@ -88,6 +89,7 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
const body = await response.json();
unmountIslands(results.current);
setScope(body.scope);
setTabs(body.tabs);
setHtml(body.results);
setCrop(body.crop);
@@ -118,7 +120,7 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
// A link that changes what is shown, in whichever look the control has.
const choice = (params, label, active, className) => (
<Choice key={label} params={params} search={search} active={active} className={className} onChoose={show}>{label}</Choice>
<Choice key={label} params={params} search={pendingSearch} active={active} className={className} onChoose={show}>{label}</Choice>
);
// A filter chip: choosing it adds that filter to the others in force, and
// choosing it again takes it off.
@@ -133,10 +135,20 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
const chooseGardener = (name) => {
if ((query.get('gardener') || '') !== name.trim()) show(addressWith({gardener: name.trim() || null}, search));
};
// The address as it will be once a gardener typed but not yet entered is
// counted, so choosing another control does not lose the name.
const pendingSearch = (() => {
if (!gardeners) return search;
const pending = new URLSearchParams(search);
if (gardener.trim()) pending.set('gardener', gardener.trim());
else pending.delete('gardener');
return pending.size ? `?${pending}` : '';
})();
const filtering = Object.keys(clear).some((key) => query.has(key));
return (
<>
<div dangerouslySetInnerHTML={{__html: scope}} onClick={onContentClick} />
<div dangerouslySetInnerHTML={{__html: tabs}} onClick={onContentClick} />
<div className="seed-filters">
<div className="seed-filter-row">
@@ -147,7 +159,7 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
{crop ? (
<span className="seed-crop">
{crop.name}
<Choice params={{crop: null}} search={search} className="seed-crop-clear" onChoose={show}>
<Choice params={{crop: null}} search={pendingSearch} className="seed-crop-clear" onChoose={show}>
<span aria-hidden="true">×</span>
<span className="visually-hidden">Clear crop</span>
</Choice>
@@ -159,14 +171,14 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
autoFocus={false}
label="Search seeds by crop"
placeholder={placeholder}
onChoose={(chosen) => show(addressWith({crop: chosen.slug}, search))}
onChoose={(chosen) => show(addressWith({crop: chosen.slug}, pendingSearch))}
/>
)}
</div>
</div>
)}
{filtering && (
<Choice params={clear} search={search} className="seed-clear" onChoose={show}>Clear all filters</Choice>
<Choice params={clear} search={pendingSearch} className="seed-clear" onChoose={show}>Clear all filters</Choice>
)}
</div>
<div className="seed-filter-row">
@@ -176,7 +188,7 @@ export default function SeedLibrary({tabs: initialTabs, html: initialHtml, group
id="seed-month"
className="seed-month form-select form-select-sm"
value={query.get('sow_month') || ''}
onChange={(event) => show(addressWith({sow_month: event.target.value || null}, search))}
onChange={(event) => show(addressWith({sow_month: event.target.value || null}, pendingSearch))}
>
<option value="">Any month</option>
<option value="now">{sowNow || 'Sow now'}</option>
+22 -1
View File
@@ -10,6 +10,7 @@ vi.mock('./CropPicker', () => ({
}));
const props = {
scope: '<nav class="seed-scope"><a href="/seeds?from=retail">Everyone</a></nav>',
tabs: '<nav><a href="/seeds?tab=mine">Mine</a></nav>',
html: '<p>First results</p><ul class="pagination"><li><a href="/seeds?page=2">2</a></li></ul>',
groups: [['crop', 'Crop'], ['year', 'Year']],
@@ -33,7 +34,7 @@ const props = {
placeholder: 'Find a crop',
};
function stubFetch(body = {tabs: '<nav />', results: '<p>Next results</p>', crop: null}) {
function stubFetch(body = {scope: '<nav class="seed-scope"><a href="/seeds">Everyone</a></nav>', tabs: '<nav />', results: '<p>Next results</p>', crop: null}) {
const fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => body}));
vi.stubGlobal('fetch', fetch);
return fetch;
@@ -160,6 +161,26 @@ describe('SeedLibrary', () => {
expect(screen.getByLabelText('Month to sow')).toBeInTheDocument();
});
it('shows the scope Rails rendered', () => {
setup();
expect(screen.getByRole('link', {name: 'Everyone'})).toHaveAttribute('href', '/seeds?from=retail');
});
it('shows the new scope when the results change', async () => {
stubFetch({scope: '<a href="/seeds?from=bought">Everyone now</a>', tabs: '<nav />', results: '<p>Next</p>', crop: null});
setup();
await userEvent.click(screen.getByRole('link', {name: 'Bought'}));
expect(await screen.findByRole('link', {name: 'Everyone now'})).toBeInTheDocument();
});
it('keeps a gardener typed but not entered when another filter is chosen', async () => {
const fetch = stubFetch();
setup();
await userEvent.type(screen.getByLabelText('Gardener'), 'Fiona');
await userEvent.click(screen.getByRole('link', {name: 'Bought'}));
expect(fetch).toHaveBeenLastCalledWith('/seeds?gardener=Fiona&from=bought', expect.anything());
});
it('labels the planted filter', () => {
setup();
expect(screen.getByText('Planted', {selector: '.seed-field-label'})).toBeInTheDocument();