diff --git a/frontend/src/components/inventory/InventoryInlineRow.tsx b/frontend/src/components/inventory/InventoryInlineRow.tsx
index f338786..adc2af1 100644
--- a/frontend/src/components/inventory/InventoryInlineRow.tsx
+++ b/frontend/src/components/inventory/InventoryInlineRow.tsx
@@ -1,4 +1,3 @@
-import { useMemo } from 'react';
import type { MouseEvent } from 'react';
import {
Autocomplete,
@@ -14,6 +13,7 @@ import CheckIcon from '@mui/icons-material/Check';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import type { InventoryItem, OrgInventoryItem } from '../../services/inventory.service';
import type { FocusController } from '../../utils/focusController';
+import { useMemoizedLocations } from '../../hooks/useMemoizedLocations';
export type InventoryRecord = InventoryItem | OrgInventoryItem;
@@ -73,31 +73,18 @@ export const InventoryInlineRow = ({
typeof inlineDraft.locationId === 'string'
? Number(inlineDraft.locationId)
: inlineDraft.locationId;
- const selectedLocation =
- allLocations.find((loc) => loc.id === draftLocationId) ||
- (typeof draftLocationId === 'number'
- ? {
- id: draftLocationId,
- name: item.locationName || `Location #${draftLocationId}`,
- }
- : null);
- const filteredOptions = useMemo(() => {
- const filterTerm = inlineLocationInput.trim().toLowerCase();
- return allLocations
- .filter((opt) => opt.name.toLowerCase().includes(filterTerm))
- .sort((a, b) => {
- const aName = a.name.toLowerCase();
- const bName = b.name.toLowerCase();
- const aStarts = aName.startsWith(filterTerm);
- const bStarts = bName.startsWith(filterTerm);
- if (aStarts !== bStarts) return aStarts ? -1 : 1;
- const aIndex = aName.indexOf(filterTerm);
- const bIndex = bName.indexOf(filterTerm);
- if (aIndex !== bIndex) return aIndex - bIndex;
- return a.name.localeCompare(b.name);
- });
- }, [allLocations, inlineLocationInput]);
+ const { filtered: filteredOptions, getSelected } = useMemoizedLocations(
+ allLocations,
+ inlineLocationInput,
+ );
+ const selectedLocation =
+ typeof draftLocationId === 'number'
+ ? getSelected(draftLocationId) ||
+ (item.locationName
+ ? { id: draftLocationId, name: item.locationName }
+ : null)
+ : null;
const draftQuantityNumber = Number(inlineDraft.quantity);
diff --git a/frontend/src/hooks/useMemoizedLocations.ts b/frontend/src/hooks/useMemoizedLocations.ts
new file mode 100644
index 0000000..1239456
--- /dev/null
+++ b/frontend/src/hooks/useMemoizedLocations.ts
@@ -0,0 +1,31 @@
+import { useMemo } from 'react';
+
+interface LocationOption {
+ id: number;
+ name: string;
+}
+
+export const useMemoizedLocations = (allLocations: LocationOption[], input: string) => {
+ const filtered = useMemo(() => {
+ const term = input.trim().toLowerCase();
+ return allLocations
+ .filter((opt) => opt.name.toLowerCase().includes(term))
+ .sort((a, b) => {
+ const aName = a.name.toLowerCase();
+ const bName = b.name.toLowerCase();
+ const aStarts = aName.startsWith(term);
+ const bStarts = bName.startsWith(term);
+ if (aStarts !== bStarts) return aStarts ? -1 : 1;
+ const aIndex = aName.indexOf(term);
+ const bIndex = bName.indexOf(term);
+ if (aIndex !== bIndex) return aIndex - bIndex;
+ return a.name.localeCompare(b.name);
+ });
+ }, [allLocations, input]);
+
+ const getSelected = (id: number | '') =>
+ typeof id === 'number' ? allLocations.find((loc) => loc.id === id) ?? null : null;
+
+ return { filtered, getSelected };
+};
+
diff --git a/frontend/src/pages/Inventory.editor-mode.test.tsx b/frontend/src/pages/Inventory.editor-mode.test.tsx
index eedefad..20c16eb 100644
--- a/frontend/src/pages/Inventory.editor-mode.test.tsx
+++ b/frontend/src/pages/Inventory.editor-mode.test.tsx
@@ -40,6 +40,13 @@ jest.mock('../services/uex.service', () => ({
getStarSystems: jest.fn(),
},
}));
+jest.mock('../hooks/useMemoizedLocations', () => {
+ const original = jest.requireActual('../hooks/useMemoizedLocations');
+ return {
+ ...original,
+ useMemoizedLocations: jest.fn((...args: unknown[]) => original.useMemoizedLocations(...args)),
+ };
+});
const mockItem = {
id: 'item-1',
userId: 1,
@@ -446,4 +453,14 @@ describe('Inventory editor mode inline controls', () => {
const saveButton = await screen.findByTestId('inline-save-item-1');
await waitFor(() => expect(document.activeElement).toBe(saveButton));
});
+
+ it('memoizes location filtering for inline rows', () => {
+ const { useMemoizedLocations: mockedHook } = jest.requireMock('../hooks/useMemoizedLocations');
+ render(
+
+
+ ,
+ );
+ expect(mockedHook).toHaveBeenCalled();
+ });
});
diff --git a/frontend/src/pages/Inventory.tsx b/frontend/src/pages/Inventory.tsx
index 3025390..e21b99c 100644
--- a/frontend/src/pages/Inventory.tsx
+++ b/frontend/src/pages/Inventory.tsx
@@ -1439,6 +1439,89 @@ const InventoryPage = () => {
}
const showEmptyState = filteredItems.length === 0 && !refreshing;
+ const renderInlineRow = (item: InventoryRecord) => {
+ const rowKey = item.id.toString();
+ const draft = inlineDrafts[item.id] ?? {
+ locationId: Number(item.locationId) || '',
+ quantity: Number(item.quantity) || 0,
+ };
+ const originalLocationId = Number(item.locationId) || '';
+ const draftLocationId =
+ typeof draft.locationId === 'string' ? Number(draft.locationId) : draft.locationId;
+ const originalQuantity = Number(item.quantity) || 0;
+ const draftQuantityNumber = Number(draft.quantity);
+ const isDirty =
+ draftLocationId !== originalLocationId || draftQuantityNumber !== originalQuantity;
+ const inlineLocationValue =
+ inlineLocationInputs[rowKey] ??
+ (locationEditing[rowKey]
+ ? ''
+ : allLocations.find((loc) => loc.id === (typeof draft.locationId === 'number' ? draft.locationId : Number(draft.locationId)))?.name ??
+ item.locationName ??
+ '');
+ const saving = inlineSaving.has(item.id);
+ const errorText = inlineError[item.id];
+
+ return (
+ setInlineDraft(item.id, changes)}
+ onErrorChange={(message) =>
+ setInlineError((prev) => ({
+ ...prev,
+ [item.id]: message,
+ }))
+ }
+ onLocationInputChange={(value) =>
+ setInlineLocationInputs((prev) => ({
+ ...prev,
+ [rowKey]: value,
+ }))
+ }
+ onLocationFocus={() => {
+ setInlineLocationInputs((prev) => ({
+ ...prev,
+ [rowKey]: '',
+ }));
+ setLocationEditing((prev) => ({ ...prev, [rowKey]: true }));
+ setInlineError((prev) => ({ ...prev, [item.id]: null }));
+ }}
+ onLocationBlur={(selectedName) => {
+ setInlineLocationInputs((prev) => ({
+ ...prev,
+ [rowKey]:
+ selectedName ??
+ allLocations.find((loc) => loc.id === draftLocationId)?.name ??
+ '',
+ }));
+ setLocationEditing((prev) => ({ ...prev, [rowKey]: false }));
+ setInlineError((prev) => ({ ...prev, [item.id]: null }));
+ }}
+ onSave={() => handleInlineSaveAndAdvance(item)}
+ onOpenActions={(e) => handleActionOpen(e, item)}
+ setLocationRef={(ref, key) => {
+ locationRefs.current[key] = ref;
+ }}
+ setQuantityRef={(ref, key) => {
+ quantityRefs.current[key] = ref;
+ }}
+ setSaveRef={(ref, key) => {
+ saveRefs.current[key] = ref;
+ }}
+ />
+ );
+ };
return (
@@ -1745,84 +1828,7 @@ const InventoryPage = () => {
}>
- {groupItems.map((item) => {
- const rowKey = item.id.toString();
- const draft = inlineDrafts[item.id] ?? {
- locationId: Number(item.locationId) || '',
- quantity: Number(item.quantity) || 0,
- };
- const originalLocationId = Number(item.locationId) || '';
- const originalQuantity = Number(item.quantity) || 0;
- const draftLocationId =
- typeof draft.locationId === 'string'
- ? Number(draft.locationId)
- : draft.locationId;
- const saving = inlineSaving.has(item.id);
- const errorText = inlineError[item.id];
- const draftQuantityNumber = Number(draft.quantity);
- const isDirty =
- draftLocationId !== originalLocationId ||
- draftQuantityNumber !== originalQuantity;
- return (
- setInlineDraft(item.id, changes)}
- onErrorChange={(message) =>
- setInlineError((prev) => ({ ...prev, [item.id]: message }))
- }
- onLocationInputChange={(value) =>
- setInlineLocationInputs((prev) => ({
- ...prev,
- [rowKey]: value,
- }))
- }
- onLocationFocus={() => {
- setInlineLocationInputs((prev) => ({
- ...prev,
- [rowKey]: '',
- }));
- setLocationEditing((prev) => ({ ...prev, [rowKey]: true }));
- setInlineError((prev) => ({ ...prev, [item.id]: null }));
- }}
- onLocationBlur={(selectedName) => {
- setInlineLocationInputs((prev) => ({
- ...prev,
- [rowKey]:
- selectedName ??
- allLocations.find((loc) => loc.id === draftLocationId)?.name ??
- '',
- }));
- setLocationEditing((prev) => ({ ...prev, [rowKey]: false }));
- setInlineError((prev) => ({ ...prev, [item.id]: null }));
- }}
- onSave={() => handleInlineSaveAndAdvance(item)}
- onOpenActions={(e) => handleActionOpen(e, item)}
- setLocationRef={(ref, key) => {
- locationRefs.current[key] = ref;
- }}
- setQuantityRef={(ref, key) => {
- quantityRefs.current[key] = ref;
- }}
- setSaveRef={(ref, key) => {
- saveRefs.current[key] = ref;
- }}
- />
- );
- })}
+ {groupItems.map((item) => renderInlineRow(item))}
))}