fix(admin): theme, American English, and inventory/reservation tooling (#27)
Seven reported items, of which the first four had two root causes. The active tab was invisible in dark mode because colorPrimary was hardcoded to #1a1a1a in both themes. The accent now inverts with the theme, and colorTextLightSolid inverts with it, or a near-white accent would get antd's default white label and disappear. The Category tab, Tag tab, and item-form category selector ignored the theme entirely. antd declares main: lib/index.js and module: es/index.js, so importing from 'antd' resolves to the ES build while 'antd/lib/...' loads the CommonJS one — two copies, two React contexts, and no ConfigProvider for anything deep-imported. Switching those files to antd/es/* keeps the deep-import convention and shares the instance. This was introduced by my own use of the lib path; es is correct under Vite. Two storefront components had the same latent bug. "Colour" is now "Color". The Customers tab shows how many items each customer is holding, as a link opening the item list with a Release button. Release mirrors the customer's own cart removal — drop the cart row, return the item to available, guarded on 'reserved' so it can never resurrect a sold item — and deliberately sends no email about an action the customer did not take. The count is a subquery rather than another join, which would have multiplied rows and inflated order_count and total_spent_cents. The Inventory tab filters by category, tags, price, and status, reusing the storefront's parser and query builder so the two cannot drift. Reserved is one option in a Status filter rather than a standalone toggle. Also fixes two defects the screenshots exposed: the reserved-count link bubbled to the row handler and opened the customer drawer behind the dialog, and .admin-category-node had no CSS at all, so the tree node name, item count, and actions ran together as one string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,8 @@ describe('parseItemFilters', () => {
|
||||
categoryId: null,
|
||||
tagIds: [],
|
||||
minPriceCents: null,
|
||||
maxPriceCents: null
|
||||
maxPriceCents: null,
|
||||
status: null
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,6 +73,25 @@ describe('parseItemFilters', () => {
|
||||
it('rejects a repeated query param rather than guessing which one to use', () => {
|
||||
expect(() => parseItemFilters({ category: ['1', '2'] })).toThrow(FilterError);
|
||||
});
|
||||
|
||||
it('parses each of the item statuses', () => {
|
||||
expect(parseItemFilters({ status: 'available' }).status).toBe('available');
|
||||
expect(parseItemFilters({ status: 'reserved' }).status).toBe('reserved');
|
||||
expect(parseItemFilters({ status: 'sold' }).status).toBe('sold');
|
||||
});
|
||||
|
||||
it('treats an absent or empty status as no status filter', () => {
|
||||
expect(parseItemFilters({}).status).toBeNull();
|
||||
expect(parseItemFilters({ status: '' }).status).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects a status outside the known set', () => {
|
||||
expect(() => parseItemFilters({ status: 'pending' })).toThrow(FilterError);
|
||||
});
|
||||
|
||||
it('rejects a status differing only by case, rather than silently coercing it', () => {
|
||||
expect(() => parseItemFilters({ status: 'Reserved' })).toThrow(FilterError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildItemFilterSql', () => {
|
||||
@@ -100,6 +120,12 @@ describe('buildItemFilterSql', () => {
|
||||
expect(built.clauses.join(' ')).toContain('$3');
|
||||
});
|
||||
|
||||
it('filters on status', () => {
|
||||
const built = buildItemFilterSql(parseItemFilters({ status: 'reserved' }), 1);
|
||||
expect(built.clauses.join(' ')).toContain('i.status');
|
||||
expect(built.params).toEqual(['reserved']);
|
||||
});
|
||||
|
||||
it('continues numbering across multiple filters', () => {
|
||||
const built = buildItemFilterSql(
|
||||
parseItemFilters({ category: '4', min_price: '100', max_price: '900' }),
|
||||
|
||||
Reference in New Issue
Block a user