Add inactive item fallback for dashboard change feed lookups
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+5
-2
@@ -116,8 +116,11 @@ export async function listGroupedStockEntries(store, options = {}) {
|
|||||||
return fetchAllListPages(store, `${getPath('items')}/grouped`, baseQuery);
|
return fetchAllListPages(store, `${getPath('items')}/grouped`, baseQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStockEntry(store, stockId) {
|
export async function getStockEntry(store, stockId, { allowInactive = false } = {}) {
|
||||||
const payload = await apiRequest(store, `${getPath('items')}/${stockId}`);
|
const path = `${getPath('items')}/${stockId}`;
|
||||||
|
const payload = allowInactive
|
||||||
|
? await apiRequest(store, path, { query: { allow_inactive: 1 } })
|
||||||
|
: await apiRequest(store, path);
|
||||||
return unwrapEntryPayload(payload);
|
return unwrapEntryPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,18 @@ export function dashboardPageData(store) {
|
|||||||
|
|
||||||
if (missingItemUuids.length) {
|
if (missingItemUuids.length) {
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
missingItemUuids.map((uuid) => getStockEntry(store, uuid)),
|
missingItemUuids.map(async (uuid) => {
|
||||||
|
try {
|
||||||
|
return await getStockEntry(store, uuid);
|
||||||
|
} catch (error) {
|
||||||
|
const status = error?.status || error?.cause?.status;
|
||||||
|
if (status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getStockEntry(store, uuid, { allowInactive: true });
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ vi.mock('../../src/api/client.js', () => ({
|
|||||||
const {
|
const {
|
||||||
adjustStockEntry,
|
adjustStockEntry,
|
||||||
applyItemUpsert,
|
applyItemUpsert,
|
||||||
|
getStockEntry,
|
||||||
listGroupedStockEntries,
|
listGroupedStockEntries,
|
||||||
listKitchenChanges,
|
listKitchenChanges,
|
||||||
listStockEntries,
|
listStockEntries,
|
||||||
@@ -125,6 +126,35 @@ describe('api/stock', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getStockEntry fetches item without allow_inactive by default', async () => {
|
||||||
|
apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-1', name: 'Milk' });
|
||||||
|
|
||||||
|
const result = await getStockEntry({ config: { database: 'db' } }, 'item-1');
|
||||||
|
|
||||||
|
expect(result).toEqual({ uuid_b64: 'item-1', name: 'Milk' });
|
||||||
|
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||||
|
{ config: { database: 'db' } },
|
||||||
|
'kitchen/items/item-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getStockEntry forwards allow_inactive when requested', async () => {
|
||||||
|
apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-2', active: false });
|
||||||
|
|
||||||
|
const result = await getStockEntry(
|
||||||
|
{ config: { database: 'db' } },
|
||||||
|
'item-2',
|
||||||
|
{ allowInactive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ uuid_b64: 'item-2', active: false });
|
||||||
|
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||||
|
{ config: { database: 'db' } },
|
||||||
|
'kitchen/items/item-2',
|
||||||
|
{ query: { allow_inactive: 1 } },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('listKitchenChanges returns normalized changes payload', async () => {
|
it('listKitchenChanges returns normalized changes payload', async () => {
|
||||||
apiRequestMock.mockResolvedValueOnce({
|
apiRequestMock.mockResolvedValueOnce({
|
||||||
since: 'cursor-1',
|
since: 'cursor-1',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const listKitchenChangesMock = vi.fn();
|
const listKitchenChangesMock = vi.fn();
|
||||||
const getStockEntryMock = vi.fn();
|
const getStockEntryMock = vi.fn();
|
||||||
@@ -16,6 +16,12 @@ vi.mock('../../../src/api/locations.js', () => ({
|
|||||||
const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js');
|
const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js');
|
||||||
|
|
||||||
describe('features/dashboard/dashboard-page', () => {
|
describe('features/dashboard/dashboard-page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
listKitchenChangesMock.mockReset();
|
||||||
|
getStockEntryMock.mockReset();
|
||||||
|
fetchLocationsMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders dashboard with recent changes section', () => {
|
it('renders dashboard with recent changes section', () => {
|
||||||
const html = renderDashboardPage();
|
const html = renderDashboardPage();
|
||||||
expect(html).toContain('Recent changes');
|
expect(html).toContain('Recent changes');
|
||||||
@@ -107,6 +113,50 @@ describe('features/dashboard/dashboard-page', () => {
|
|||||||
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
|
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('retries stock event item lookup with allowInactive after 404', async () => {
|
||||||
|
listKitchenChangesMock.mockResolvedValueOnce({
|
||||||
|
since: null,
|
||||||
|
nextCursor: null,
|
||||||
|
changes: [{
|
||||||
|
type: 'stock',
|
||||||
|
action: 'upsert',
|
||||||
|
timestamp: '2026-04-10T10:00:00Z',
|
||||||
|
stock: {
|
||||||
|
item_uuid_b64: 'item-uuid-2',
|
||||||
|
quantity: 1,
|
||||||
|
uom_symbol: 'pcs',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
getStockEntryMock
|
||||||
|
.mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 }))
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
uuid_b64: 'item-uuid-2',
|
||||||
|
name: 'Archived pasta',
|
||||||
|
stock_type: 'measured',
|
||||||
|
});
|
||||||
|
fetchLocationsMock.mockResolvedValueOnce({ flat: [] });
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
isConnected: true,
|
||||||
|
setActiveKitchen: vi.fn(),
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
};
|
||||||
|
const data = dashboardPageData(store);
|
||||||
|
|
||||||
|
await data.refreshChanges();
|
||||||
|
|
||||||
|
expect(getStockEntryMock).toHaveBeenNthCalledWith(1, store, 'item-uuid-2');
|
||||||
|
expect(getStockEntryMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
store,
|
||||||
|
'item-uuid-2',
|
||||||
|
{ allowInactive: true },
|
||||||
|
);
|
||||||
|
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Archived pasta');
|
||||||
|
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 1 pcs');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps empty state when API returns no changes', async () => {
|
it('keeps empty state when API returns no changes', async () => {
|
||||||
listKitchenChangesMock.mockResolvedValueOnce({
|
listKitchenChangesMock.mockResolvedValueOnce({
|
||||||
since: null,
|
since: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user