feat(admin): filter inventory by status directly, so Published and Unpublished are reachable (#132)
There was no way to find unpublished items. Every item has arrived pending since #90 and has to be published, so "what is waiting for me to publish" is a routine question the inventory could not answer. #105 replaced the four-way status dropdown with a Sold / Not sold / All preset and recorded at the time that this gave up isolating a single status, that the pending workflow was the likeliest thing to miss it, and that the fix would be to restore the ability rather than remove the preset. That turned out to be right, and sooner than expected. The admin now selects statuses directly - Pending, Available, Reserved, Sold - rather than choosing among presets over them. The API has accepted several statuses since #105, so this exposes the dimension itself. Everything becomes expressible in one control: Unpublished is Pending, Published is the other three, Sold and Not sold are the sets they always were, and Reserved on its own is reachable again. Two alternatives were rejected. Growing the preset list to five would have kept one click per answer while leaving Reserved unreachable and growing again at the next new question. A second control for publication beside the one for availability would have read more naturally and reintroduced exactly what #105 was built to avoid: Sold and Unpublished is an impossible pair, since a sold item is necessarily published, and two dimensions have to either give that a meaning or block it. One dimension cannot contradict itself. The storefront keeps its three-way preset unchanged. Pending is excluded from every public read, so Published and Unpublished are not distinctions a customer can draw, and the simpler control is the right one there. An empty selection means no filter rather than no statuses, or clearing the box would empty the table. Verification: the admin filter spec is rewritten rather than deleted, and now asserts what the preset could not - Pending alone finds the staged fixture and hides the published ones, and Available plus Reserved plus Sold finds the published ones and hides the staged one. That second case is the one a preset would have had to be invented for. All 7 admin filter tests pass, along with 122 of the suite. Two locator details cost time and are written into the spec so they do not have to be rediscovered: antd renders an invisible role="listbox" shim beside the real option list, so getByRole('option') resolves something zero-sized that cannot be clicked; and a selected status renders as a tag carrying the same title as its option, so an unscoped getByTitle becomes ambiguous once anything is chosen. Beyond the two pre-existing password-reset failures that need a database on port 55432, two storefront specs failed under the full concurrent run and pass six-for-six in isolation, twice. That is the shared-database contention filed as #116, not a regression here: this change touches the admin only. Closes #132 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,27 @@
|
||||
import { useMemo } from 'react';
|
||||
import TreeSelect from 'antd/es/tree-select';
|
||||
import Select from 'antd/es/select';
|
||||
import Segmented from 'antd/es/segmented';
|
||||
import InputNumber from 'antd/es/input-number';
|
||||
import Button from 'antd/es/button';
|
||||
import type { Category, Tag } from '../api';
|
||||
import {
|
||||
ItemFilters,
|
||||
SaleState,
|
||||
ADMIN_SALE_STATUSES,
|
||||
ItemStatus,
|
||||
buildCategoryTree,
|
||||
CategoryNode,
|
||||
hasActiveFilters,
|
||||
saleStateFromStatuses
|
||||
hasActiveFilters
|
||||
} from '../filters';
|
||||
|
||||
// Named individually rather than grouped, because grouping is what the preset
|
||||
// this replaces did. Pending is listed first: "what is waiting to be published"
|
||||
// is the question that prompted #132.
|
||||
const STATUS_OPTIONS: { value: ItemStatus; label: string }[] = [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'available', label: 'Available' },
|
||||
{ value: 'reserved', label: 'Reserved' },
|
||||
{ value: 'sold', label: 'Sold' }
|
||||
];
|
||||
|
||||
interface CategoryTreeOption {
|
||||
value: number;
|
||||
title: string;
|
||||
@@ -93,32 +100,34 @@ export default function InventoryFilters({ categories, tags, filters, onChange,
|
||||
onChange={(value) => onChange({ ...filters, maxPriceCents: dollarsToCents(value) })}
|
||||
/>
|
||||
|
||||
{/* Replaces the four-way status dropdown that used to sit here. One
|
||||
control instead of two overlapping ways to say the same thing.
|
||||
Note what it costs: a single status can no longer be isolated, so
|
||||
there is no longer a way to view only Reserved, or only Pending.
|
||||
Not Sold folds pending in with available and reserved. If isolating
|
||||
one status turns out to matter — the pending workflow from #90 is the
|
||||
likeliest candidate — the fix is to put that back alongside this
|
||||
preset, not to remove it. See the decision recorded on #105. */}
|
||||
<Segmented
|
||||
aria-label="Filter by availability"
|
||||
value={saleStateFromStatuses(filters.status, ADMIN_SALE_STATUSES, 'all')}
|
||||
onChange={(value) => {
|
||||
const state = value as SaleState;
|
||||
onChange({
|
||||
...filters,
|
||||
// All is the admin's default, so it is held as "no preference"
|
||||
// rather than as a list naming every status — which keeps it out of
|
||||
// the active-filter count and out of Clear filters' way.
|
||||
status: state === 'all' ? null : ADMIN_SALE_STATUSES[state]
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Not sold', value: 'not-sold' },
|
||||
{ label: 'Sold', value: 'sold' },
|
||||
{ label: 'All', value: 'all' }
|
||||
]}
|
||||
{/* The status dimension itself rather than presets over it, which #105's
|
||||
Sold / Not sold / All control was. Presets could not express Published
|
||||
or Unpublished, could not isolate Reserved, and would have grown a new
|
||||
button for every new question. Selecting statuses answers all of them:
|
||||
Unpublished is Pending, Published is the other three, and Not sold is
|
||||
everything except Sold.
|
||||
|
||||
A second control for publication would have read more naturally and
|
||||
reintroduced what #105 avoided — Sold and Unpublished is an impossible
|
||||
pair, since a sold item is necessarily published. One dimension cannot
|
||||
contradict itself. See #132.
|
||||
|
||||
The storefront keeps the three-way preset: pending is excluded from
|
||||
every public read, so Published and Unpublished are not distinctions a
|
||||
customer can draw. */}
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="Any status"
|
||||
aria-label="Filter by status"
|
||||
style={{ minWidth: 220 }}
|
||||
value={filters.status ?? []}
|
||||
onChange={(value: ItemStatus[]) =>
|
||||
// Empty means no filter, not "no statuses". A multi-select cleared
|
||||
// back to nothing should show everything rather than an empty table.
|
||||
onChange({ ...filters, status: value.length ? value : null })
|
||||
}
|
||||
options={STATUS_OPTIONS}
|
||||
/>
|
||||
|
||||
{hasActiveFilters(filters) && <Button onClick={onClear}>Clear filters</Button>}
|
||||
|
||||
@@ -38,14 +38,12 @@ export const STOREFRONT_SALE_STATUSES: Record<SaleState, ItemStatus[]> = {
|
||||
all: ['available', 'reserved', 'sold']
|
||||
};
|
||||
|
||||
// In the admin, Not Sold includes pending: an item awaiting publication has
|
||||
// certainly not been sold, and hiding it from the default view would hide the
|
||||
// items most likely to need attention.
|
||||
export const ADMIN_SALE_STATUSES: Record<SaleState, ItemStatus[]> = {
|
||||
'not-sold': ['available', 'reserved', 'pending'],
|
||||
sold: ['sold'],
|
||||
all: ['available', 'reserved', 'sold', 'pending']
|
||||
};
|
||||
// The admin had a table of its own here until #132, where the preset was
|
||||
// replaced by a multi-select of the statuses themselves. Presets could not
|
||||
// express Published or Unpublished and could not isolate a single status, and
|
||||
// the admin is where those questions get asked. The storefront keeps its
|
||||
// preset: pending never reaches a customer, so the distinction does not exist
|
||||
// for them.
|
||||
|
||||
function isPublicStatus(value: string): value is ItemStatus {
|
||||
return value === 'available' || value === 'reserved' || value === 'sold';
|
||||
|
||||
Reference in New Issue
Block a user