Skip to content

Commit

Permalink
chore: improve domain selector functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
svedova committed Feb 23, 2024
1 parent 2528ffe commit 1accfab
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 110 deletions.
74 changes: 53 additions & 21 deletions src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SelectProps } from "@mui/material/Select";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import FilledInput from "@mui/material/FilledInput";
import OutlinedInput from "@mui/material/OutlinedInput";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
Expand All @@ -16,32 +17,52 @@ interface MenuItem {

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

export default function MultiSelect({
items,
helperText,
selected = [],
selected,
variant = "filled",
size,
label,
placeholder,
multiple = true,
fullWidth = true,
onSelect,
}: Props) {
const [selectedItems, setSelectedItems] = useState<string[]>(selected);
const [selectedItems, setSelectedItems] = useState<string[]>(selected || []);

useEffect(() => {
if (selected) {
setSelectedItems(selected);
}
}, [selected]);

return (
<FormControl variant="standard" fullWidth>
<InputLabel id="multiple-checkbox-label" sx={{ pl: 2, pt: 1 }} shrink>
{label}
</InputLabel>
<FormControl variant="standard" fullWidth={fullWidth}>
{label && (
<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 />}
multiple={multiple}
variant={variant}
size={size}
value={selectedItems?.length ? selectedItems : [""]}
input={variant === "filled" ? <FilledInput /> : <OutlinedInput />}
onClose={() => {
// Rely on `onClose` only when `multiple` property is true.
if (multiple) {
onSelect(selectedItems.filter(i => i));
}
}}
onChange={e => {
let values = [
...new Set(
Expand All @@ -58,19 +79,30 @@ export default function MultiSelect({
}

setSelectedItems(values);
onSelect(values.filter(i => i));

// If not multiple, trigger the select here.
if (!multiple) {
onSelect(values.filter(i => i));
}
}}
renderValue={selected => {
return selected.filter(s => s !== "").join(", ") || "All hosts";
return selected.filter(s => s !== "").join(", ") || placeholder;
}}
>
<MenuItem>
<Checkbox checked={!selectedItems.filter(i => i).length} />
<ListItemText primary={"All hosts"} />
</MenuItem>
{multiple ? (
<MenuItem>
<Checkbox checked={!selectedItems.filter(i => i).length} />
<ListItemText primary={placeholder} />
</MenuItem>
) : (
// Fixes an issue with MUI Warning
<MenuItem value={""} sx={{ display: "none" }} />
)}
{items.map(item => (
<MenuItem key={item.value} value={item.value}>
<Checkbox checked={selectedItems.includes(item.value)} />
{multiple && (
<Checkbox checked={selectedItems.includes(item.value)} />
)}
<ListItemText primary={item.text} />
</MenuItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/analytics/Analytics.tsx", () =
envId: currentEnv.id!,
appId: currentApp.id!,
status: 200,
verified: true,
response: {
domains: hasDomain
? [{ domainName: "www.stormkit.io", verified: true, id: "15" }]
Expand Down Expand Up @@ -101,7 +102,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/analytics/Analytics.tsx", () =
await waitFor(() => {
expect(
wrapper.getByText("Setup a custom domain").getAttribute("href")
).toBe(`/apps/${currentApp.id}/environments/${currentEnv.id}#domain`);
).toBe(`/apps/${currentApp.id}/environments/${currentEnv.id}#domains`);
});
});
});
Expand Down
15 changes: 10 additions & 5 deletions src/pages/apps/[id]/environments/[env-id]/analytics/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function Analytics() {
}}
>
<Link
href={`/apps/${environment.appId}/environments/${environment.id}#domain`}
href={`/apps/${environment.appId}/environments/${environment.id}#domains`}
sx={{
color: "white",
":hover": { color: "inherit", textDecoration: "none" },
Expand All @@ -56,15 +56,20 @@ export default function Analytics() {
subtitle="Monitor user analytics for the specified domain within this environment configuration."
actions={
<DomainSelector
selected={domain ? [domain.domainName] : []}
appId={environment.appId}
envId={environment.id!}
onDomainSelect={d => {
if (d === null) {
setNoDomainYet(true);
fullWidth={false}
onFetch={d => {
if (d?.[0]) {
setDomain(d[0]);
} else {
setDomain(d);
setNoDomainYet(true);
}
}}
onDomainSelect={d => {
setDomain(d ? (d[0] as Domain) : undefined);
}}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RenderResult } from "@testing-library/react";
import type { Scope } from "nock";
import { MemoryRouter } from "react-router";
import { waitFor, fireEvent, render, getByText } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
Expand All @@ -8,6 +9,7 @@ import {
mockUpdateSnippet,
mockInsertSnippet,
} from "~/testing/nocks/nock_snippets";
import { mockFetchDomains } from "~/testing/nocks/nock_domains";
import mockApp from "~/testing/data/mock_app";
import mockEnvironment from "~/testing/data/mock_environment";
import SnippetModal from "./SnippetModal";
Expand All @@ -32,6 +34,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
let snippets: Snippet[];
let closeModal: jest.Mock;
let setRefreshToken: jest.Mock;
let fetchDomainsScope: Scope;

const snippet: Snippet = {
title: "Google Analytics",
Expand All @@ -41,10 +44,22 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
location: "head",
};

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

const createWrapper = ({ app, env, closeModal, snippet }: Props) => {
fetchDomainsScope = mockFetchDomains({
appId: app.id,
envId: env.id!,
verified: true,
response: {
domains: [
{ domainName: "www.e.org", verified: true, id: "2" },
{ domainName: "e.org", verified: true, id: "2" },
],
},
});

wrapper = render(
<MemoryRouter>
<AppContext.Provider
Expand All @@ -58,7 +73,6 @@ 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 All @@ -83,6 +97,12 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
});
});

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

test("should handle form submission", async () => {
const scope = mockInsertSnippet({
appId: currentApp.id,
Expand All @@ -102,6 +122,9 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
fireEvent.click(findOption("www.e.org"));
fireEvent.click(findOption("e.org"));

// Closes the dropdown
await userEvent.keyboard("{Escape}");

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

await waitFor(() => {
Expand Down Expand Up @@ -139,6 +162,7 @@ describe("~/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx", ()
location: "body",
title: "Hotjar",
id: 1,
rules: {},
},
});

Expand Down
55 changes: 27 additions & 28 deletions src/pages/apps/[id]/environments/[env-id]/snippets/SnippetModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useContext, FormEventHandler } from "react";
import { useState, useContext, FormEventHandler } from "react";
import { html } from "@codemirror/lang-html";
import CodeMirror from "@uiw/react-codemirror";
import Box from "@mui/material/Box";
Expand All @@ -11,18 +11,17 @@ 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";
import { AppContext } from "~/pages/apps/[id]/App.context";
import { EnvironmentContext } from "~/pages/apps/[id]/environments/Environment.context";
import DomainSelector from "~/shared/domains/DomainSelector";
import Modal from "~/components/Modal";
import { addSnippet, updateSnippet } from "./actions";

interface Props {
snippet?: Snippet;
domains?: string[];
closeModal: () => void;
setRefreshToken: (t: number) => void;
}
Expand All @@ -39,21 +38,20 @@ interface FormValues {
| "body_prepend";
}

const SnippetModal: React.FC<Props> = ({
const defaultContent = "<script>\n console.log('Hello world');\n</script>";

export default function SnippetModal({
closeModal,
snippet,
setRefreshToken,
domains,
}): React.ReactElement => {
}: Props) {
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>"
const [selectedHosts, setSelectedHosts] = useState<string[]>();
const [codeContent, setCodeContent] = useState<string>(
snippet?.content || defaultContent
);

const handleSubmit: FormEventHandler = e => {
Expand All @@ -79,7 +77,10 @@ const SnippetModal: React.FC<Props> = ({
enabled: values.enabled === "on",
location: location === "head" ? "head" : "body",
prepend: prependOrAppend === "prepend",
rules: selectedHosts?.length ? { hosts: selectedHosts } : undefined,
rules: {
...snippet?.rules,
hosts: selectedHosts ? selectedHosts : snippet?.rules?.hosts,
},
},
})
.then(() => {
Expand All @@ -102,7 +103,7 @@ const SnippetModal: React.FC<Props> = ({
<Modal open onClose={closeModal}>
<Card component="form" error={error} onSubmit={handleSubmit}>
<CardHeader
title={snippet?.id ? "Edit snippet" : "Create snippet"}
title={snippet?.id ? `Edit snippet #${snippet.id}` : "Create snippet"}
subtitle={
<>
Snippets will be injected during response time into your document.
Expand Down Expand Up @@ -164,24 +165,24 @@ const SnippetModal: React.FC<Props> = ({
Append to Body {"(inserted after <body>)"}
</Option>
<Option value="body_prepend">
Prepend to Body {"(inserted before </head>)"}
Prepend to Body {"(inserted before </body>)"}
</Option>
</Select>
</FormControl>
</Box>
<Box sx={{ mb: 4 }}>
<MultiSelect
label="Hosts"
items={[
{ value: "*.dev", text: "All development endpoints (*.dev)" },
...(domains?.length
? domains?.map(d => ({ value: d, text: d }))
: []),
]}
selected={selectedHosts}
onSelect={value => {
setSelectedHosts(value);
<DomainSelector
variant="filled"
label="Domains"
appId={app.id}
envId={environment.id!}
selected={snippet?.rules?.hosts}
onDomainSelect={value => {
setSelectedHosts(value as string[]);
}}
multiple
withDevDomains
fullWidth
/>
</Box>
<Box sx={{ bgcolor: "rgba(0,0,0,0.2)", p: 1.75, pt: 1, mb: 2 }}>
Expand Down Expand Up @@ -219,6 +220,4 @@ const SnippetModal: React.FC<Props> = ({
</Card>
</Modal>
);
};

export default SnippetModal;
}
Loading

0 comments on commit 1accfab

Please sign in to comment.