Skip to content

Commit

Permalink
feat: combine import/export buttons
Browse files Browse the repository at this point in the history
Signed-off-by: rare-magma <[email protected]>
  • Loading branch information
rare-magma committed Mar 16, 2024
1 parent ad6dba1 commit 90e5508
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 118 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ Keyboard shortcuts can be triggered when no input field is selected.
| Redo last change | R |
| Clone current budget | C |
| Create new budget | A |
| Open import/export panel | O |
| Export data as JSON | S |
| Export current budget as CSV | D |
| Open settings panel | T |
| Go to older budget | PageDown |
| Go to current month's budget | Home |
| Go to newer budget | PageUp |
Expand Down
14 changes: 10 additions & 4 deletions e2e/settingsHappyPath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ test("should complete the settings happy path", async ({ page, isMobile }) => {
await page.getByLabel("EUR").click();

await expect(page.getByText("€0.00")).toBeVisible();
await expect(page.getByText("CSV")).toBeVisible();

await page.getByLabel("import or export budget").click();
await expect(page.getByText("csv")).toBeVisible();

// should handle downloads
const csvDownloadPromise = page.waitForEvent("download");
Expand All @@ -54,8 +56,8 @@ test("should complete the settings happy path", async ({ page, isMobile }) => {
(await fs.promises.stat(await csvDownload.path())).size,
).toBeGreaterThan(0);

await page.getByLabel("budget settings").click();
await expect(page.getByText("JSON")).toBeVisible();
await page.getByLabel("import or export budget").click();
await expect(page.getByText("json")).toBeVisible();

const jsonDownloadPromise = page.waitForEvent("download");
await page.getByLabel("export budget as json").click();
Expand All @@ -66,7 +68,11 @@ test("should complete the settings happy path", async ({ page, isMobile }) => {
).toBeGreaterThan(0);

// should handle import
await page.locator("#import").setInputFiles("./docs/guitos-sample.json");
await expect(page.getByLabel("import or export budget")).toBeVisible();
await page.getByLabel("import or export budget").click();
await page
.getByTestId("import-form-control")
.setInputFiles("./docs/guitos-sample.json");

await expect(page.getByLabel("go to older budget")).toBeVisible();
await page.getByLabel("go to older budget").click();
Expand Down
21 changes: 15 additions & 6 deletions src/components/NavBar/NavBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("NavBar", () => {
});

it("triggers event when import button is pressed", async () => {
await userEvent.click(screen.getByLabelText("import budget"));
await userEvent.click(screen.getByLabelText("import or export budget"));
await userEvent.upload(
screen.getByTestId("import-form-control"),
new File([JSON.stringify(testBudget)], "budget", {
Expand All @@ -64,20 +64,20 @@ describe("NavBar", () => {
});

it("triggers event when export shortcuts are pressed", async () => {
await userEvent.type(screen.getByTestId("header"), "t");
await userEvent.type(screen.getByTestId("header"), "o");
expect(
screen.getByRole("button", {
name: /export budget as csv/i,
name: /import budget/i,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /export budget as json/i,
name: /export budget as csv/i,
}),
).toBeInTheDocument();
expect(
screen.getByRole("link", {
name: /open guitos changelog/i,
screen.getByRole("button", {
name: /export budget as json/i,
}),
).toBeInTheDocument();

Expand All @@ -86,6 +86,15 @@ describe("NavBar", () => {
await userEvent.type(screen.getByTestId("header"), "d");
});

it("triggers event when settings shortcuts are pressed", async () => {
await userEvent.type(screen.getByTestId("header"), "t");
expect(
screen.getByRole("link", {
name: /open guitos changelog/i,
}),
).toBeInTheDocument();
});

it("triggers event when user changes budget name input", async () => {
setBudgetMock.mockClear();
await userEvent.type(screen.getByDisplayValue("2023-03"), "change name");
Expand Down
44 changes: 2 additions & 42 deletions src/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
BsArrowRight,
BsPlusLg,
BsQuestionLg,
BsUpload,
BsXLg,
} from "react-icons/bs";
import { FaRegClone } from "react-icons/fa";
Expand All @@ -28,6 +27,7 @@ import { useMove } from "../../hooks/useMove";
import { focusRef, getLabelKey } from "../../utils";
import "./NavBar.css";
import { NavBarDelete } from "./NavBarDelete";
import { NavBarImpExp } from "./NavBarImpExp";
import { NavBarItem } from "./NavBarItem";
import { NavBarSettings } from "./NavBarSettings";

Expand All @@ -38,8 +38,6 @@ export interface SearchOption {
}

export function NavBar() {
const importRef =
useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const searchRef = useRef<TypeaheadRef>(null);
const nameRef =
useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
Expand All @@ -58,7 +56,6 @@ export function NavBar() {
deleteBudget,
renameBudget,
searchBudgets,
handleImport,
} = useDB();

const { budget, undo, redo, canRedo, canUndo, budgetNameList } = useBudget();
Expand Down Expand Up @@ -348,44 +345,7 @@ export function NavBar() {
</>
)}

<Nav className="m-2">
<OverlayTrigger
delay={250}
placement="bottom"
overlay={
<Tooltip
id={`tooltip-import-budget`}
style={{ position: "fixed" }}
>
import budget
</Tooltip>
}
>
<Form.Group controlId="import">
<Button
className="w-100"
aria-label="import budget"
variant="outline-primary"
onClick={() => importRef.current?.click()}
>
{expanded ? "import" : <BsUpload aria-hidden />}
</Button>
<Form.Control
data-testid="import-form-control"
type="file"
multiple
ref={importRef}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
setExpanded(false);
handleImport(event);
}}
style={{ display: "none" }}
/>
</Form.Group>
</OverlayTrigger>
</Nav>
<NavBarImpExp expanded={expanded} setExpanded={setExpanded} />

{hasOneOrMoreBudgets && <NavBarSettings expanded={expanded} />}

Expand Down
196 changes: 196 additions & 0 deletions src/components/NavBar/NavBarImpExp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { Dispatch, SetStateAction, useRef } from "react";
import {
Button,
Form,
InputGroup,
Nav,
OverlayTrigger,
Popover,
Stack,
Tooltip,
} from "react-bootstrap";
import { useHotkeys } from "react-hotkeys-hook";
import { BsArrowDownUp, BsUpload } from "react-icons/bs";
import { useBudget } from "../../context/BudgetContext";
import { useDB } from "../../hooks/useDB";
import { budgetToCsv } from "../../utils";

interface NavBarImpExpProps {
expanded: boolean;
setExpanded: Dispatch<SetStateAction<boolean>>;
}

export function NavBarImpExp({ expanded, setExpanded }: NavBarImpExpProps) {
const { budget, budgetList, budgetNameList } = useBudget();
const { handleImport } = useDB();
const importRef =
useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const importButtonRef = useRef<HTMLButtonElement>(null);
const exportCSVButtonRef = useRef<HTMLButtonElement>(null);
const impExpButtonRef = useRef<HTMLButtonElement>(null);
const hasOneOrMoreBudgets = budgetNameList && budgetNameList.length > 0;

useHotkeys("o", (e) => !e.repeat && impExpButtonRef.current?.click(), {
preventDefault: true,
});

useHotkeys("s", (e) => !e.repeat && handleExportJSON(), {
preventDefault: true,
});

useHotkeys("d", (e) => !e.repeat && handleExportCSV(), {
preventDefault: true,
});

function handleExportJSON() {
if (budget) {
const date = new Date().toISOString();
const filename = `guitos-${date.slice(0, -5)}.json`;
const url = window.URL.createObjectURL(
new Blob([JSON.stringify(budgetList)]),
);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
}
}

function handleExportCSV() {
if (budget) {
const filename = `${budget.name}.csv`;
const url = window.URL.createObjectURL(new Blob([budgetToCsv(budget)]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
}
}

return hasOneOrMoreBudgets ? (
<Nav className="m-2">
<OverlayTrigger
trigger="click"
key="nav-imp-exp-overlay"
placement="bottom"
rootClose={true}
overlay={
<Popover id={`nav-popover-imp-exp-button`}>
<Popover.Body>
<Stack gap={3}>
<Stack className="align-self-center" direction="horizontal">
<InputGroup
size="sm"
className="mb-1"
key={`export-button-group`}
>
<Button
aria-label="import budget"
variant="outline-primary"
type="button"
ref={importButtonRef}
onClick={() => importRef.current?.click()}
>
import
</Button>
<Form.Control
data-testid="import-form-control"
className="straight-corners"
type="file"
multiple
ref={importRef}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
setExpanded(false);
handleImport(event);
}}
style={{ display: "none" }}
/>
<Button
id={"budget-export-csv-button"}
aria-label="export budget as csv"
key={"budget-export-csv-button"}
variant="outline-primary"
type="button"
ref={exportCSVButtonRef}
style={{
minWidth: "7ch",
}}
onClick={handleExportCSV}
>
csv
</Button>
<Button
id={"budget-export-json-button"}
aria-label="export budget as json"
key={"budget-export-json-button"}
variant="outline-primary"
type="button"
style={{
minWidth: "7ch",
}}
onClick={handleExportJSON}
>
json
</Button>
</InputGroup>
</Stack>
</Stack>
</Popover.Body>
</Popover>
}
>
<Button
className="w-100"
aria-label="import or export budget"
variant="outline-primary"
ref={impExpButtonRef}
onClick={() => {
setTimeout(() => {
importButtonRef.current?.focus();
}, 0);
}}
>
{expanded ? "import/export" : <BsArrowDownUp aria-hidden />}
</Button>
</OverlayTrigger>
</Nav>
) : (
<Nav className="m-2">
<OverlayTrigger
delay={250}
placement="bottom"
overlay={
<Tooltip id={`tooltip-import-budget`} style={{ position: "fixed" }}>
import budget
</Tooltip>
}
>
<Form.Group controlId="import">
<Button
className="w-100"
aria-label="import budget"
variant="outline-primary"
onClick={() => importRef.current?.click()}
>
{expanded ? "import" : <BsUpload aria-hidden />}
</Button>
<Form.Control
data-testid="import-form-control"
type="file"
multiple
ref={importRef}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setExpanded(false);
handleImport(event);
}}
style={{ display: "none" }}
/>
</Form.Group>
</OverlayTrigger>
</Nav>
);
}
Loading

0 comments on commit 90e5508

Please sign in to comment.