From 48fc5f3e36cd7ada4380e4d15272d197dc626be2 Mon Sep 17 00:00:00 2001 From: Can Eldem Date: Tue, 20 Feb 2024 23:11:52 +0000 Subject: [PATCH] chore: use dropdown for snippet host selection --- src/components/MultiSelect/MultiSelect.tsx | 81 +++++++++++++++++++ src/components/MultiSelect/index.ts | 1 + .../[env-id]/snippets/SnippetModal.spec.tsx | 31 +++++-- .../[env-id]/snippets/SnippetModal.tsx | 32 ++++---- .../[env-id]/snippets/Snippets.spec.tsx | 14 ++++ .../[env-id]/snippets/Snippets.tsx | 14 ++++ 6 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 src/components/MultiSelect/MultiSelect.tsx create mode 100644 src/components/MultiSelect/index.ts diff --git a/src/components/MultiSelect/MultiSelect.tsx b/src/components/MultiSelect/MultiSelect.tsx new file mode 100644 index 00000000..be4708b1 --- /dev/null +++ b/src/components/MultiSelect/MultiSelect.tsx @@ -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 { + 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(selected); + + return ( + + + {label} + + + {helperText && {helperText}} + + ); +} diff --git a/src/components/MultiSelect/index.ts b/src/components/MultiSelect/index.ts new file mode 100644 index 00000000..c728fbf2 --- /dev/null +++ b/src/components/MultiSelect/index.ts @@ -0,0 +1 @@ +export { default } from "./MultiSelect"; diff --git a/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.spec.tsx b/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.spec.tsx index 5bdf1d4c..05f4e767 100644 --- a/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.spec.tsx +++ b/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.spec.tsx @@ -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"; @@ -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( @@ -55,6 +58,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", () @@ -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(); }); }); @@ -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")); diff --git a/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx b/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx index 12997b9b..ed2139cd 100644 --- a/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx +++ b/src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx @@ -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"; @@ -21,6 +22,7 @@ import { addSnippet, updateSnippet } from "./actions"; interface Props { snippet?: Snippet; + domains?: string[]; closeModal: () => void; setRefreshToken: (t: number) => void; } @@ -41,11 +43,15 @@ const SnippetModal: React.FC = ({ closeModal, snippet, setRefreshToken, + domains, }): React.ReactElement => { const { app } = useContext(AppContext); const { environment } = useContext(EnvironmentContext); const [error, setError] = useState(); const [loading, setLoading] = useState(false); + const [selectedHosts, setSelectedHosts] = useState( + snippet?.rules?.hosts || [] + ); const [codeContent, setCodeContent] = useState( snippet?.content || "" ); @@ -62,10 +68,6 @@ const SnippetModal: React.FC = ({ ) 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, @@ -77,7 +79,7 @@ const SnippetModal: React.FC = ({ enabled: values.enabled === "on", location: location === "head" ? "head" : "body", prepend: prependOrAppend === "prepend", - rules: hosts?.length ? { hosts } : undefined, + rules: selectedHosts?.length ? { hosts: selectedHosts } : undefined, }, }) .then(() => { @@ -110,6 +112,7 @@ const SnippetModal: React.FC = ({ } /> + = ({ - ({ value: d, text: d })) + : []), + ]} + selected={selectedHosts} + onSelect={value => { + setSelectedHosts(value); }} /> diff --git a/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.spec.tsx b/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.spec.tsx index df79a803..e4963ac1 100644 --- a/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.spec.tsx +++ b/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.spec.tsx @@ -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"; @@ -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( 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]; diff --git a/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx b/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx index 7577f7f2..457ad087 100644 --- a/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx +++ b/src/pages/apps/[id]/environments/[env-id]/snippets/Snippets.tsx @@ -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"; @@ -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 ( `} + + {snippet.rules?.hosts?.join(", ") || "All hosts"} + {isSnippetModalOpen && snippets && ( {