The classification filter now offers only the loaded opportunities' classifications
It lists the classifications the opportunities in the days window have, not every one stored, plus any already chosen, so a selection stays visible and can be removed when nothing in the window has it. The other filters don't narrow it, so choosing a status doesn't make classifications vanish.
This commit is contained in:
1 parent
0819fb442b
commit
6fbf7e56b2
4 files changed
+46
-5
No files matched your search
+3
-2
@@ -69,8 +69,9 @@ again.
|
||||
|
||||
The list is fetched a page (100) at a time until there are no more. The marks
|
||||
are `interested`, `skipped`, `bid_submitted`, `lost`, `won` and `hidden`;
|
||||
Won and Lost exclude each other (409). The classifications are the ones the
|
||||
filter offers: only those some opportunity has. The client fetches the CSRF
|
||||
Won and Lost exclude each other (409). The classifications are every one some
|
||||
stored opportunity has; the filter offers only those the loaded opportunities
|
||||
(the days window) have, and any already chosen. The client fetches the CSRF
|
||||
token when it first needs it, and a new one if a request is refused for it.
|
||||
|
||||
## Where things are
|
||||
|
||||
+12
-1
@@ -6,6 +6,7 @@ import { OpportunityTable } from '@/components/OpportunityTable';
|
||||
import { buttonPrimary } from '@/components/styles';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { ToastList } from '@/components/ToastList';
|
||||
import { classificationsInUse } from '@/lib/classifications';
|
||||
import type { OpportunityFilters } from '@/lib/filterOpportunities';
|
||||
import {
|
||||
emptyFilters,
|
||||
@@ -44,6 +45,16 @@ export function App(): JSX.Element {
|
||||
const theme = createTheme();
|
||||
|
||||
const statuses = createMemo(() => statusOptions(opportunities.items()));
|
||||
// Only the classifications of the opportunities loaded (the days window),
|
||||
// not those the other filters leave: choosing a status shouldn't make
|
||||
// classifications vanish from the picker.
|
||||
const pickable = createMemo(() =>
|
||||
classificationsInUse(
|
||||
classifications.items(),
|
||||
opportunities.items(),
|
||||
filters().classificationIds,
|
||||
),
|
||||
);
|
||||
// annotations.get reads from a store, so hiding an opportunity re-runs this.
|
||||
const rows = createMemo(() =>
|
||||
filterOpportunities(opportunities.items(), filters(), annotations.get),
|
||||
@@ -89,7 +100,7 @@ export function App(): JSX.Element {
|
||||
onChange={setFilters}
|
||||
onClear={clear}
|
||||
statuses={statuses()}
|
||||
classifications={classifications.items()}
|
||||
classifications={pickable()}
|
||||
isLoadingClassifications={classifications.isLoading()}
|
||||
classificationsError={classifications.error()}
|
||||
onRetryClassifications={classifications.reload}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { ClassificationCode } from '@/types/Opportunities';
|
||||
import { applyClassificationChanges } from './classifications';
|
||||
import { applyClassificationChanges, classificationsInUse } from './classifications';
|
||||
import { testOpportunity } from './testOpportunity';
|
||||
|
||||
const code = (id: number, value: string): ClassificationCode => ({
|
||||
id,
|
||||
@@ -27,3 +28,14 @@ test('an added code replaces one with the same id', () => {
|
||||
});
|
||||
expect(after).toEqual([code(1, '03000001')]);
|
||||
});
|
||||
|
||||
test('classificationsInUse keeps those the opportunities have, and the selected ones', () => {
|
||||
const all = [code(1, '03000000'), code(2, '45000000'), code(3, '72000000'), code(4, '90000000')];
|
||||
const opportunities = [
|
||||
testOpportunity('a', { classifications: [code(3, '72000000')] }),
|
||||
testOpportunity('b', { classifications: [code(1, '03000000'), code(3, '72000000')] }),
|
||||
testOpportunity('c', { classifications: null }),
|
||||
];
|
||||
const inUse = classificationsInUse(all, opportunities, new Set([4]));
|
||||
expect(inUse.map((c) => c.id)).toEqual([1, 3, 4]);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClassificationsUpdatedPayload } from '@/types/Events';
|
||||
import type { ClassificationCode } from '@/types/Opportunities';
|
||||
import type { ClassificationCode, Opportunity } from '@/types/Opportunities';
|
||||
|
||||
/**
|
||||
* The classifications after change: without the removed ids, with the added
|
||||
@@ -15,3 +15,20 @@ export function applyClassificationChanges(
|
||||
(first, second) => first.code.localeCompare(second.code) || first.id - second.id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The classifications some of the opportunities have, and those whose ids
|
||||
* are selected, so a selection stays visible (and can be removed) when no
|
||||
* opportunity loaded has it.
|
||||
*/
|
||||
export function classificationsInUse(
|
||||
classifications: readonly ClassificationCode[],
|
||||
opportunities: readonly Opportunity[],
|
||||
selected: ReadonlySet<number>,
|
||||
): ClassificationCode[] {
|
||||
const used = new Set(selected);
|
||||
for (const opportunity of opportunities) {
|
||||
for (const code of opportunity.classifications ?? []) used.add(code.id);
|
||||
}
|
||||
return classifications.filter((code) => used.has(code.id));
|
||||
}
|
||||
Reference in new issue
Block a user