diff --git a/packages/graph-explorer/src/modules/NodeExpand/NodeExpandContent.tsx b/packages/graph-explorer/src/modules/NodeExpand/NodeExpandContent.tsx index 1bfc510e4..d1ff68344 100644 --- a/packages/graph-explorer/src/modules/NodeExpand/NodeExpandContent.tsx +++ b/packages/graph-explorer/src/modules/NodeExpand/NodeExpandContent.tsx @@ -92,10 +92,13 @@ function ExpansionOptions({ ); const [filters, setFilters] = useState>([]); const [limitEnabled, setLimitEnabled] = useState(Boolean(defaultLimit)); - const [limit, setLimit] = useState(defaultLimit ?? 100); + const [limit, setLimit] = useState(defaultLimit ?? 100); const hasSelectedType = Boolean(selectedType); const hasUnfetchedNeighbors = (neighbors?.unfetched ?? 0) > 0; + // An empty or invalid limit must block expansion, otherwise the request + // would go out unlimited while the toggle still shows a limit is applied + const hasValidLimit = !limitEnabled || (limit != null && limit >= 1); // Reset filters when selected type changes const [prevSelectedType, setPrevSelectedType] = useState(selectedType); @@ -129,7 +132,9 @@ function ExpansionOptions({ /> void; +}) { + const [limit, setLimit] = useState(100); + return ( + {}} + filters={[]} + onFiltersChange={() => {}} + limit={limit} + onLimitChange={value => { + setLimit(value); + onLimitChange(value); + }} + limitEnabled={true} + onLimitEnabledToggle={() => {}} + /> + ); +} + +function renderFilters(onLimitChange: (limit: number | null) => void) { + const state = new DbState(); + const store = getAppStore(); + state.applyTo(store); + render( + + + + + , + ); + return screen.getByLabelText("limit"); +} + +describe("NodeExpandFilters", () => { + it("reports null when the limit input is cleared", async () => { + const user = userEvent.setup(); + const onLimitChange = vi.fn(); + const input = renderFilters(onLimitChange); + + await user.clear(input); + + expect(onLimitChange).toHaveBeenLastCalledWith(null); + expect(input).toHaveValue(null); + }); + + it("reports the parsed number when the limit changes", async () => { + const user = userEvent.setup(); + const onLimitChange = vi.fn(); + const input = renderFilters(onLimitChange); + + await user.clear(input); + await user.type(input, "50"); + + expect(onLimitChange).toHaveBeenLastCalledWith(50); + expect(input).toHaveValue(50); + }); +}); diff --git a/packages/graph-explorer/src/modules/NodeExpand/NodeExpandFilters.tsx b/packages/graph-explorer/src/modules/NodeExpand/NodeExpandFilters.tsx index 0e240b22d..ee9a8db54 100644 --- a/packages/graph-explorer/src/modules/NodeExpand/NodeExpandFilters.tsx +++ b/packages/graph-explorer/src/modules/NodeExpand/NodeExpandFilters.tsx @@ -37,8 +37,9 @@ export type NodeExpandFiltersProps = { onSelectedTypeChange(type: string): void; filters: Array; onFiltersChange(filters: Array): void; - limit: number; - onLimitChange(limit: number): void; + /** The neighbor limit, or null when the input is empty or invalid. */ + limit: number | null; + onLimitChange(limit: number | null): void; limitEnabled: boolean; onLimitEnabledToggle(enabled: boolean): void; }; @@ -192,8 +193,11 @@ const NodeExpandFilters = ({ type="number" min={1} step={1} - value={limit} - onChange={e => onLimitChange(parseInt(e.target.value) ?? 0)} + value={limit ?? ""} + onChange={e => { + const parsed = parseInt(e.target.value); + onLimitChange(Number.isNaN(parsed) ? null : parsed); + }} /> )}