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
|
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`;
|
are `interested`, `skipped`, `bid_submitted`, `lost`, `won` and `hidden`;
|
||||||
Won and Lost exclude each other (409). The classifications are the ones the
|
Won and Lost exclude each other (409). The classifications are every one some
|
||||||
filter offers: only those some opportunity has. The client fetches the CSRF
|
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.
|
token when it first needs it, and a new one if a request is refused for it.
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
|
|||||||
+12
-1
@@ -6,6 +6,7 @@ import { OpportunityTable } from '@/components/OpportunityTable';
|
|||||||
import { buttonPrimary } from '@/components/styles';
|
import { buttonPrimary } from '@/components/styles';
|
||||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||||
import { ToastList } from '@/components/ToastList';
|
import { ToastList } from '@/components/ToastList';
|
||||||
|
import { classificationsInUse } from '@/lib/classifications';
|
||||||
import type { OpportunityFilters } from '@/lib/filterOpportunities';
|
import type { OpportunityFilters } from '@/lib/filterOpportunities';
|
||||||
import {
|
import {
|
||||||
emptyFilters,
|
emptyFilters,
|
||||||
@@ -44,6 +45,16 @@ export function App(): JSX.Element {
|
|||||||
const theme = createTheme();
|
const theme = createTheme();
|
||||||
|
|
||||||
const statuses = createMemo(() => statusOptions(opportunities.items()));
|
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.
|
// annotations.get reads from a store, so hiding an opportunity re-runs this.
|
||||||
const rows = createMemo(() =>
|
const rows = createMemo(() =>
|
||||||
filterOpportunities(opportunities.items(), filters(), annotations.get),
|
filterOpportunities(opportunities.items(), filters(), annotations.get),
|
||||||
@@ -89,7 +100,7 @@ export function App(): JSX.Element {
|
|||||||
onChange={setFilters}
|
onChange={setFilters}
|
||||||
onClear={clear}
|
onClear={clear}
|
||||||
statuses={statuses()}
|
statuses={statuses()}
|
||||||
classifications={classifications.items()}
|
classifications={pickable()}
|
||||||
isLoadingClassifications={classifications.isLoading()}
|
isLoadingClassifications={classifications.isLoading()}
|
||||||
classificationsError={classifications.error()}
|
classificationsError={classifications.error()}
|
||||||
onRetryClassifications={classifications.reload}
|
onRetryClassifications={classifications.reload}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
import type { ClassificationCode } from '@/types/Opportunities';
|
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 => ({
|
const code = (id: number, value: string): ClassificationCode => ({
|
||||||
id,
|
id,
|
||||||
@@ -27,3 +28,14 @@ test('an added code replaces one with the same id', () => {
|
|||||||
});
|
});
|
||||||
expect(after).toEqual([code(1, '03000001')]);
|
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 { 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
|
* 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,
|
(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