Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: use dropdown for snipped host selection #538

Merged
merged 1 commit into from
Feb 21, 2024
Merged
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
81 changes: 81 additions & 0 deletions src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { SelectProps } from "@mui/material/Select";
import React, { useState } from "react";
import FilledInput from "@mui/material/FilledInput";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import ListItemText from "@mui/material/ListItemText";
import Select from "@mui/material/Select";
import Checkbox from "@mui/material/Checkbox";
import FormHelperText from "@mui/material/FormHelperText";

interface MenuItem {
value: string;
text: string;
}

interface Props extends Omit<SelectProps, "onSelect"> {
items: MenuItem[];
selected: string[];
onSelect: (v: string[]) => void;
helperText?: React.ReactNode;
label?: string;
}

export default function MultiSelect({
items,
helperText,
selected = [],
label,
onSelect,
}: Props) {
const [selectedItems, setSelectedItems] = useState<string[]>(selected);

return (
<FormControl variant="standard" fullWidth>
<InputLabel id="multiple-checkbox-label" sx={{ pl: 2, pt: 1 }} shrink>
{label}
</InputLabel>
<Select
labelId="multiple-checkbox-label"
multiple
variant="filled"
value={selectedItems.length > 0 ? selectedItems : [""]}
input={<FilledInput />}
onChange={e => {
let values = [
...new Set(
typeof e.target.value === "string"
? e.target.value.split(",")
: e.target.value
),
];

const lastClicked = values.at(-1);

if (typeof lastClicked === "undefined") {
values = [];
}

setSelectedItems(values);
onSelect(values.filter(i => i));
}}
renderValue={selected => {
return selected.filter(s => s !== "").join(", ") || "All hosts";
}}
>
<MenuItem>
<Checkbox checked={!selectedItems.filter(i => i).length} />
<ListItemText primary={"All hosts"} />
</MenuItem>
{items.map(item => (
<MenuItem key={item.value} value={item.value}>
<Checkbox checked={selectedItems.includes(item.value)} />
<ListItemText primary={item.text} />
</MenuItem>
))}
</Select>
{helperText && <FormHelperText>{helperText}</FormHelperText>}
</FormControl>
);
}
1 change: 1 addition & 0 deletions src/components/MultiSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./MultiSelect";
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RenderResult } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { waitFor, fireEvent, render } from "@testing-library/react";
import { waitFor, fireEvent, render, getByText } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AppContext } from "~/pages/apps/[id]/App.context";
import { EnvironmentContext } from "~/pages/apps/[id]/environments/Environment.context";
Expand Down Expand Up @@ -41,6 +41,9 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
location: "head",
};

const findDropdown = () => wrapper.getByLabelText("Hosts");
const findOption = (text: string) => getByText(document.body, text);

const createWrapper = ({ app, env, closeModal, snippet }: Props) => {
wrapper = render(
<MemoryRouter>
Expand All @@ -55,6 +58,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
<SnippetModal
setRefreshToken={setRefreshToken}
closeModal={closeModal}
domains={["www.e.org", "e.org"]}
snippet={snippet}
/>
</EnvironmentContext.Provider>
Expand Down Expand Up @@ -83,20 +87,28 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
const scope = mockInsertSnippet({
appId: currentApp.id,
envId: currentEnv.id!,
snippets: [{ ...snippet, rules: { hosts: ["www.e.org", "e.org"] } }],
snippets: [
{ ...snippet, rules: { hosts: ["*.dev", "www.e.org", "e.org"] } },
],
});

await userEvent.type(wrapper.getByLabelText("Title"), "Google Analytics");
await userEvent.type(wrapper.getByLabelText("Hosts"), "www.e.org, e.org");

const selector = findDropdown();
expect(selector).toBeTruthy();
fireEvent.mouseDown(selector);

fireEvent.click(findOption("All development endpoints (*.dev)"));
fireEvent.click(findOption("www.e.org"));
fireEvent.click(findOption("e.org"));

await fireEvent.click(wrapper.getByText("Create"));

await waitFor(() => {
expect(scope.isDone()).toBe(true);
expect(closeModal).toHaveBeenCalled();
expect(setRefreshToken).toHaveBeenCalled();
});

expect(closeModal).toHaveBeenCalled();
expect(setRefreshToken).toHaveBeenCalled();
});
});

Expand All @@ -122,7 +134,12 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
const scope = mockUpdateSnippet({
appId: currentApp.id,
envId: currentEnv.id!,
snippet: { ...snippet, location: "body", title: "Hotjar", id: 1 },
snippet: {
...snippet,
location: "body",
title: "Hotjar",
id: 1,
},
});

await userEvent.clear(wrapper.getByLabelText("Title"));
Expand Down
32 changes: 18 additions & 14 deletions src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Option from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import InputLabel from "@mui/material/InputLabel";
import MultiSelect from "~/components/MultiSelect";
import Card from "~/components/Card";
import CardHeader from "~/components/CardHeader";
import CardFooter from "~/components/CardFooter";
Expand All @@ -21,6 +22,7 @@ import { addSnippet, updateSnippet } from "./actions";

interface Props {
snippet?: Snippet;
domains?: string[];
closeModal: () => void;
setRefreshToken: (t: number) => void;
}
Expand All @@ -41,11 +43,15 @@ const SnippetModal: React.FC<Props> = ({
closeModal,
snippet,
setRefreshToken,
domains,
}): React.ReactElement => {
const { app } = useContext(AppContext);
const { environment } = useContext(EnvironmentContext);
const [error, setError] = useState<string>();
const [loading, setLoading] = useState(false);
const [selectedHosts, setSelectedHosts] = useState<string[]>(
snippet?.rules?.hosts || []
);
const [codeContent, setCodeContent] = useState(
snippet?.content || "<script>\n console.log('Hello world');\n</script>"
);
Expand All @@ -62,10 +68,6 @@ const SnippetModal: React.FC<Props> = ({
) as unknown as FormValues;

const [location, prependOrAppend] = values.injectLocation.split("_");
const hosts = values.hosts
.split(",")
.map(i => i.trim().toLowerCase())
.filter(i => i);

handler({
appId: app.id,
Expand All @@ -77,7 +79,7 @@ const SnippetModal: React.FC<Props> = ({
enabled: values.enabled === "on",
location: location === "head" ? "head" : "body",
prepend: prependOrAppend === "prepend",
rules: hosts?.length ? { hosts } : undefined,
rules: selectedHosts?.length ? { hosts: selectedHosts } : undefined,
},
})
.then(() => {
Expand Down Expand Up @@ -110,6 +112,7 @@ const SnippetModal: React.FC<Props> = ({
</>
}
/>

<Box sx={{ mb: 4 }}>
<TextField
label="Title"
Expand Down Expand Up @@ -167,16 +170,17 @@ const SnippetModal: React.FC<Props> = ({
</FormControl>
</Box>
<Box sx={{ mb: 4 }}>
<TextField
<MultiSelect
label="Hosts"
name="hosts"
fullWidth
defaultValue={snippet?.rules?.hosts?.join(", ") || ""}
variant="filled"
autoComplete="off"
helperText="Limit this snippet to specified hosts. Separate multiple hosts with a comma `,`."
InputLabelProps={{
shrink: true,
items={[
{ value: "*.dev", text: "All development endpoints (*.dev)" },
...(domains?.length
? domains?.map(d => ({ value: d, text: d }))
: []),
]}
selected={selectedHosts}
onSelect={value => {
setSelectedHosts(value);
}}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { waitFor, fireEvent, render } from "@testing-library/react";
import { AppContext } from "~/pages/apps/[id]/App.context";
import { EnvironmentContext } from "~/pages/apps/[id]/environments/Environment.context";
import { mockFetchSnippets } from "~/testing/nocks/nock_snippets";
import { mockFetchDomains } from "~/testing/nocks/nock_domains";
import mockSnippets from "~/testing/data/mock_snippets";
import mockApp from "~/testing/data/mock_app";
import mockEnvironment from "~/testing/data/mock_environment";
Expand All @@ -17,12 +18,19 @@ interface Props {

describe("~/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx", () => {
let fetchSnippetsScope: Scope;
let fetchDomainsScope: Scope;
let wrapper: RenderResult;
let currentApp: App;
let currentEnv: Environment;
let snippets = mockSnippets();

const createWrapper = ({ app, env }: Props) => {
fetchDomainsScope = mockFetchDomains({
appId: app.id,
envId: env.id!,
response: { domains: [] },
});

wrapper = render(
<MemoryRouter>
<AppContext.Provider
Expand Down Expand Up @@ -54,6 +62,12 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx", () =>
createWrapper({ app: currentApp, env: currentEnv });
});

test("should fetch domains", async () => {
await waitFor(() => {
expect(fetchDomainsScope.isDone()).toBe(true);
});
});

test("should load snippets", async () => {
const s1 = snippets[0];
const s2 = snippets[0];
Expand Down
14 changes: 14 additions & 0 deletions src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import CardFooter from "~/components/CardFooter";
import CardRow from "~/components/CardRow";
import EmptyPage from "~/components/EmptyPage";
import ConfirmModal from "~/components/ConfirmModal";
import { useFetchDomains } from "~/shared/domains/actions";
import { useFetchSnippets, deleteSnippet, updateSnippet } from "./actions";
import SnippetModal from "./SnippetModal";

Expand All @@ -30,6 +31,15 @@ export default function Snippets() {
refreshToken,
});

const domainsRes = useFetchDomains({
appId: app.id,
envId: env.id!,
});

const domains = domainsRes.domains
.filter(d => d.verified === true)
.map(d => d.domainName);

return (
<Card
error={error}
Expand Down Expand Up @@ -90,6 +100,9 @@ export default function Snippets() {
{`<${snippet.prepend ? "/" : ""}${snippet.location}>`}
</Typography>
</Typography>
<Typography sx={{ color: grey[500] }}>
{snippet.rules?.hosts?.join(", ") || "All hosts"}
</Typography>
</Box>
<FormControlLabel
sx={{ pl: 0, ml: 0 }}
Expand Down Expand Up @@ -130,6 +143,7 @@ export default function Snippets() {
</CardFooter>
{isSnippetModalOpen && snippets && (
<SnippetModal
domains={domains}
snippet={toBeModified}
setRefreshToken={setRefreshToken}
closeModal={() => {
Expand Down
Loading