Skip to content

Commit 3a1431c

Browse files
authored
Merge pull request #2843 from github/charisk/model-kind-dropdown
Update KindInput component to bring it inline with others
2 parents 73f161c + dc33784 commit 3a1431c

File tree

6 files changed

+205
-146
lines changed

6 files changed

+205
-146
lines changed

extensions/ql-vscode/src/view/model-editor/KindInput.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

extensions/ql-vscode/src/view/model-editor/MethodRow.tsx

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import { vscode } from "../vscode-api";
1111

1212
import { Method } from "../../model-editor/method";
1313
import { ModeledMethod } from "../../model-editor/modeled-method";
14-
import { KindInput } from "./KindInput";
15-
import { extensiblePredicateDefinitions } from "../../model-editor/predicates";
14+
import { ModelKindDropdown } from "./ModelKindDropdown";
1615
import { Mode } from "../../model-editor/shared/mode";
1716
import { MethodClassifications } from "./MethodClassifications";
1817
import {
@@ -73,31 +72,11 @@ export const MethodRow = (props: MethodRowProps) => {
7372
function ModelableMethodRow(props: MethodRowProps) {
7473
const { method, modeledMethod, methodIsUnsaved, mode, onChange } = props;
7574

76-
const handleKindChange = useCallback(
77-
(kind: string) => {
78-
if (!modeledMethod) {
79-
return;
80-
}
81-
82-
onChange(method, {
83-
...modeledMethod,
84-
kind,
85-
});
86-
},
87-
[onChange, method, modeledMethod],
88-
);
89-
9075
const jumpToUsage = useCallback(
9176
() => sendJumpToUsageMessage(method),
9277
[method],
9378
);
9479

95-
const predicate =
96-
modeledMethod?.type && modeledMethod.type !== "none"
97-
? extensiblePredicateDefinitions[modeledMethod.type]
98-
: undefined;
99-
const showKindCell = predicate?.supportedKinds;
100-
10180
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
10281

10382
return (
@@ -154,12 +133,10 @@ function ModelableMethodRow(props: MethodRowProps) {
154133
/>
155134
</VSCodeDataGridCell>
156135
<VSCodeDataGridCell gridColumn={5}>
157-
<KindInput
158-
kinds={predicate?.supportedKinds || []}
159-
value={modeledMethod?.kind}
160-
disabled={!showKindCell}
161-
onChange={handleKindChange}
162-
aria-label="Kind"
136+
<ModelKindDropdown
137+
method={method}
138+
modeledMethod={modeledMethod}
139+
onChange={onChange}
163140
/>
164141
</VSCodeDataGridCell>
165142
</>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as React from "react";
2+
import { ChangeEvent, useCallback, useEffect, useMemo } from "react";
3+
import type {
4+
ModeledMethod,
5+
ModeledMethodKind,
6+
} from "../../model-editor/modeled-method";
7+
import { Dropdown } from "../common/Dropdown";
8+
import { Method } from "../../model-editor/method";
9+
import { extensiblePredicateDefinitions } from "../../model-editor/predicates";
10+
11+
type Props = {
12+
method: Method;
13+
modeledMethod: ModeledMethod | undefined;
14+
onChange: (method: Method, modeledMethod: ModeledMethod) => void;
15+
};
16+
17+
export const ModelKindDropdown = ({
18+
method,
19+
modeledMethod,
20+
onChange,
21+
}: Props) => {
22+
const predicate = useMemo(() => {
23+
return modeledMethod?.type && modeledMethod.type !== "none"
24+
? extensiblePredicateDefinitions[modeledMethod.type]
25+
: undefined;
26+
}, [modeledMethod?.type]);
27+
28+
const kinds = useMemo(() => predicate?.supportedKinds || [], [predicate]);
29+
30+
const disabled = useMemo(
31+
() => !predicate?.supportedKinds,
32+
[predicate?.supportedKinds],
33+
);
34+
35+
const options = useMemo(
36+
() => kinds.map((kind) => ({ value: kind, label: kind })),
37+
[kinds],
38+
);
39+
40+
const onChangeKind = useCallback(
41+
(kind: ModeledMethodKind) => {
42+
if (!modeledMethod) {
43+
return;
44+
}
45+
46+
onChange(method, {
47+
...modeledMethod,
48+
kind,
49+
});
50+
},
51+
[method, modeledMethod, onChange],
52+
);
53+
54+
const handleChange = useCallback(
55+
(e: ChangeEvent<HTMLSelectElement>) => {
56+
const target = e.target as HTMLSelectElement;
57+
const kind = target.value;
58+
59+
onChangeKind(kind);
60+
},
61+
[onChangeKind],
62+
);
63+
64+
useEffect(() => {
65+
const value = modeledMethod?.kind;
66+
if (value === undefined && kinds.length > 0) {
67+
onChangeKind(kinds[0]);
68+
}
69+
70+
if (value !== undefined && !kinds.includes(value)) {
71+
onChangeKind(kinds[0]);
72+
}
73+
}, [modeledMethod?.kind, kinds, onChangeKind]);
74+
75+
return (
76+
<Dropdown
77+
value={modeledMethod?.kind}
78+
options={options}
79+
disabled={disabled}
80+
onChange={handleChange}
81+
aria-label="Kind"
82+
/>
83+
);
84+
};

extensions/ql-vscode/src/view/model-editor/__tests__/KindInput.spec.tsx

Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as React from "react";
2+
import { render, screen } from "@testing-library/react";
3+
import { ModelKindDropdown } from "../ModelKindDropdown";
4+
import userEvent from "@testing-library/user-event";
5+
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
6+
import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories";
7+
8+
describe(ModelKindDropdown.name, () => {
9+
const onChange = jest.fn();
10+
const method = createMethod();
11+
12+
beforeEach(() => {
13+
onChange.mockReset();
14+
});
15+
16+
it("allows changing the kind", async () => {
17+
const modeledMethod = createModeledMethod({
18+
type: "source",
19+
kind: "local",
20+
});
21+
22+
render(
23+
<ModelKindDropdown
24+
method={method}
25+
modeledMethod={modeledMethod}
26+
onChange={onChange}
27+
/>,
28+
);
29+
30+
expect(screen.getByRole("combobox")).toHaveValue("local");
31+
await userEvent.selectOptions(screen.getByRole("combobox"), "remote");
32+
expect(onChange).toHaveBeenCalledWith(
33+
method,
34+
expect.objectContaining({
35+
kind: "remote",
36+
}),
37+
);
38+
});
39+
40+
it("resets the kind when changing the supported kinds", () => {
41+
const method = createMethod();
42+
const modeledMethod = createModeledMethod({
43+
type: "source",
44+
kind: "local",
45+
});
46+
47+
const { rerender } = render(
48+
<ModelKindDropdown
49+
method={method}
50+
modeledMethod={modeledMethod}
51+
onChange={onChange}
52+
/>,
53+
);
54+
55+
expect(screen.getByRole("combobox")).toHaveValue("local");
56+
expect(onChange).not.toHaveBeenCalled();
57+
58+
// Changing the type to sink should update the supported kinds
59+
const updatedModeledMethod = createModeledMethod({
60+
type: "sink",
61+
});
62+
63+
rerender(
64+
<ModelKindDropdown
65+
method={method}
66+
modeledMethod={updatedModeledMethod}
67+
onChange={onChange}
68+
/>,
69+
);
70+
71+
expect(screen.getByRole("combobox")).toHaveValue("code-injection");
72+
});
73+
74+
it("sets the kind when value is undefined", () => {
75+
const method = createMethod();
76+
const modeledMethod = createModeledMethod({
77+
type: "source",
78+
});
79+
80+
render(
81+
<ModelKindDropdown
82+
method={method}
83+
modeledMethod={modeledMethod}
84+
onChange={onChange}
85+
/>,
86+
);
87+
88+
expect(screen.getByRole("combobox")).toHaveValue("local");
89+
expect(onChange).toHaveBeenCalledWith(
90+
method,
91+
expect.objectContaining({
92+
kind: "local",
93+
}),
94+
);
95+
});
96+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ModeledMethod } from "../../../src/model-editor/modeled-method";
2+
3+
export function createModeledMethod(
4+
data: Partial<ModeledMethod> = {},
5+
): ModeledMethod {
6+
return {
7+
libraryVersion: "1.6.0",
8+
signature: "org.sql2o.Connection#createQuery(String)",
9+
packageName: "org.sql2o",
10+
typeName: "Connection",
11+
methodName: "createQuery",
12+
methodParameters: "(String)",
13+
type: "sink",
14+
input: "Argument[0]",
15+
output: "",
16+
kind: "jndi-injection",
17+
provenance: "manual",
18+
...data,
19+
};
20+
}

0 commit comments

Comments
 (0)