Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,13 @@ function ExpansionOptions({
);
const [filters, setFilters] = useState<Array<NodeExpandFilter>>([]);
const [limitEnabled, setLimitEnabled] = useState(Boolean(defaultLimit));
const [limit, setLimit] = useState<number>(defaultLimit ?? 100);
const [limit, setLimit] = useState<number | null>(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);
Expand Down Expand Up @@ -129,7 +132,9 @@ function ExpansionOptions({
/>
<PanelFooter className="sticky bottom-0 flex flex-row justify-end">
<ExpandButton
isDisabled={!hasUnfetchedNeighbors || !hasSelectedType}
isDisabled={
!hasUnfetchedNeighbors || !hasSelectedType || !hasValidLimit
}
vertexId={vertexId}
filters={{
filterByVertexTypes: [selectedType],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";

import { TooltipProvider } from "@/components";
import { getAppStore } from "@/core";
import { createQueryClient } from "@/core/queryClient";
import { DbState, TestProvider } from "@/utils/testing";

import NodeExpandFilters from "./NodeExpandFilters";

function Harness({
onLimitChange,
}: {
onLimitChange: (limit: number | null) => void;
}) {
const [limit, setLimit] = useState<number | null>(100);
return (
<NodeExpandFilters
neighborsOptions={[{ label: "Person", value: "Person" }]}
selectedType="Person"
onSelectedTypeChange={() => {}}
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(
<TestProvider client={createQueryClient()} store={store}>
<TooltipProvider>
<Harness onLimitChange={onLimitChange} />
</TooltipProvider>
</TestProvider>,
);
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export type NodeExpandFiltersProps = {
onSelectedTypeChange(type: string): void;
filters: Array<NodeExpandFilter>;
onFiltersChange(filters: Array<NodeExpandFilter>): 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;
};
Expand Down Expand Up @@ -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);
}}
/>
</motion.div>
)}
Expand Down