From 23114877b59cb22b714e29ddd6c9d6f9e6d479ce Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 3 Apr 2024 15:21:40 -0700 Subject: [PATCH 1/6] components/tailwind: Add ld-grey --- packages/components/src/tailwind/theme/base/colors.ts | 1 + packages/components/src/tailwind/theme/hosted/colors.ts | 1 - packages/components/src/tailwind/theme/workbench/colors.ts | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/src/tailwind/theme/base/colors.ts b/packages/components/src/tailwind/theme/base/colors.ts index 9374cd14..bff6dde4 100644 --- a/packages/components/src/tailwind/theme/base/colors.ts +++ b/packages/components/src/tailwind/theme/base/colors.ts @@ -8,6 +8,7 @@ const staticColors = { "ld-darkestblue": "#182134", "ld-darkgrey": "#95a3a7", "ld-green": "#5deda2", + "ld-grey": "#f2f5fb", "ld-lightblue": "#f6f8f9", "ld-lightgrey": "#e1e5e7", "ld-lightpurple": "#f1f3f8", diff --git a/packages/components/src/tailwind/theme/hosted/colors.ts b/packages/components/src/tailwind/theme/hosted/colors.ts index b5d9de84..c3512cec 100644 --- a/packages/components/src/tailwind/theme/hosted/colors.ts +++ b/packages/components/src/tailwind/theme/hosted/colors.ts @@ -4,7 +4,6 @@ export const colors = { "ld-bluegrey": "#284f94", "ld-brightgreen": "#29e3c1", "ld-darkblue": "#183362", - "ld-grey": "#f2f5fb", "ld-orange": "#ff820f", }; diff --git a/packages/components/src/tailwind/theme/workbench/colors.ts b/packages/components/src/tailwind/theme/workbench/colors.ts index b68a54b3..69694055 100644 --- a/packages/components/src/tailwind/theme/workbench/colors.ts +++ b/packages/components/src/tailwind/theme/workbench/colors.ts @@ -5,10 +5,8 @@ export const colors = { "ld-brightgreen": "#29e3c1", "ld-darkblue": "#183362", "ld-darkerblue": "#192E3D", - "ld-grey": "#f2f5fb", "ld-mediumblue": "#2E4459", "ld-orange": "#FF7042", - "acc-hoverlinkblue": "#1f6dc6", "acc-linkblue": "#3d91f0", }; From 96f91fdb6d27cf1b2036dc6b9bdb64d6486c62b2 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 3 Apr 2024 15:51:02 -0700 Subject: [PATCH 2/6] components: Add ErrorMsg --- packages/components/src/ErrorMsg/context.tsx | 65 ++++++++++++ .../components/src/ErrorMsg/index.module.css | 3 + packages/components/src/ErrorMsg/index.tsx | 51 +++++++++ .../src/__stories__/ErrorMsg.stories.tsx | 18 ++++ .../src/__tests__/ErrorMsg.test.tsx | 100 ++++++++++++++++++ packages/components/src/index.ts | 5 + 6 files changed, 242 insertions(+) create mode 100644 packages/components/src/ErrorMsg/context.tsx create mode 100644 packages/components/src/ErrorMsg/index.module.css create mode 100644 packages/components/src/ErrorMsg/index.tsx create mode 100644 packages/components/src/__stories__/ErrorMsg.stories.tsx create mode 100644 packages/components/src/__tests__/ErrorMsg.test.tsx diff --git a/packages/components/src/ErrorMsg/context.tsx b/packages/components/src/ErrorMsg/context.tsx new file mode 100644 index 00000000..627d1231 --- /dev/null +++ b/packages/components/src/ErrorMsg/context.tsx @@ -0,0 +1,65 @@ +import React, { createContext, useCallback, useContext, useMemo } from "react"; + +type Props = { + children: React.ReactNode; + improveErrorMsgFn?: (m: string) => string; + renderDifferentComp?: (m: string) => JSX.Element | null; +}; + +type ErrorMsgContextType = { + improveErrorMsg: (m: string) => string; + renderDifferentComp?: (m: string) => JSX.Element | null; +}; + +const ErrorMsgContext = createContext({ + improveErrorMsg: (m: string) => m, + renderDifferentComp: () => null, +}); + +// ErrorMsgProvider sets improveErrorMsg and renderDifferentComp functions in +// context. Should wrap all pages that use ErrorMsg. +export default function ErrorMsgProvider({ + children, + improveErrorMsgFn, + renderDifferentComp, +}: Props) { + const improveErrorMsg = useCallback( + (msg: string): string => { + if (isTimeoutError(msg)) { + return "Request timed out. Please try again."; + } + return improveErrorMsgFn ? improveErrorMsgFn(msg) : msg; + }, + [improveErrorMsgFn], + ); + + const value = useMemo(() => { + return { improveErrorMsg, renderDifferentComp }; + }, [improveErrorMsg, renderDifferentComp]); + + return ( + + {children} + + ); +} + +export function useErrorContext(): ErrorMsgContextType { + return useContext(ErrorMsgContext); +} + +export function isTimeoutError(err: string): boolean { + return ( + err.includes("upstream request timeout") || + err.includes("query error: timeout") || + err.includes("Unexpected token 'u'") || + // Chrome and Edge + err.includes("Unexpected token u in JSON at position 0") || + // Safari + err.includes(`Unexpected identifier "upstream"`) || + // Firefox + err.includes("unexpected character at line 1 column 1 of the JSON data") || + err.includes("Failed to fetch") || + err.includes("Received status code 504") + ); +} diff --git a/packages/components/src/ErrorMsg/index.module.css b/packages/components/src/ErrorMsg/index.module.css new file mode 100644 index 00000000..87c0438c --- /dev/null +++ b/packages/components/src/ErrorMsg/index.module.css @@ -0,0 +1,3 @@ +.errorMsg { + @apply text-acc-red font-semibold mx-auto mt-6; +} diff --git a/packages/components/src/ErrorMsg/index.tsx b/packages/components/src/ErrorMsg/index.tsx new file mode 100644 index 00000000..18d94581 --- /dev/null +++ b/packages/components/src/ErrorMsg/index.tsx @@ -0,0 +1,51 @@ +import cx from "classnames"; +import React from "react"; +import { useErrorContext } from "./context"; +import css from "./index.module.css"; + +type Props = { + err?: Error; + errString?: string; + className?: string; +}; + +export default function ErrorMsg({ err, errString, className }: Props) { + const { improveErrorMsg, renderDifferentComp } = useErrorContext(); + + const msg = (() => { + if (err) return improveErrorMsg(err.message); + if (errString) return improveErrorMsg(errString); + return null; + })(); + + if (!msg) return null; + const customComp = renderDifferentComp ? renderDifferentComp(msg) : null; + if (customComp !== null) { + return React.cloneElement(customComp, { + className: cx(css.errorMsg, className), + }); + } + + const splitMsg = msg.split("\n").filter(Boolean); + return ( +
+ {splitMsg.map(m => ( +
{m}
+ ))} +
+ ); +} + +// function ConnectionLostMessage(props: { +// className?: string; +// errString: string; +// }) { +// return ( +//
+//
+// {props.errString} +//
+//
This error is often caused by a Dolt issue.
+//
+// ); +// } diff --git a/packages/components/src/__stories__/ErrorMsg.stories.tsx b/packages/components/src/__stories__/ErrorMsg.stories.tsx new file mode 100644 index 00000000..420d75be --- /dev/null +++ b/packages/components/src/__stories__/ErrorMsg.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import ErrorMsg from "../ErrorMsg"; + +const meta: Meta = { + title: "ErrorMsg", + component: ErrorMsg, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + errString: "This is an error", + }, +}; diff --git a/packages/components/src/__tests__/ErrorMsg.test.tsx b/packages/components/src/__tests__/ErrorMsg.test.tsx new file mode 100644 index 00000000..7a290aa3 --- /dev/null +++ b/packages/components/src/__tests__/ErrorMsg.test.tsx @@ -0,0 +1,100 @@ +import { initialUppercase } from "@dolthub/web-utils"; +import { render, screen } from "@testing-library/react"; +import React from "react"; +import ErrorMsg from "../ErrorMsg"; +import ErrorProvider from "../ErrorMsg/context"; + +const errMsg = "Shit, something went wrong"; +const improve = (m: string) => initialUppercase(m.replace("Shit, ", "")); +const improvedMsg = "Something went wrong"; +function Custom(props: { className?: string }) { + return
Custom message
; +} +const renderDifferentComp = (m: string) => { + if (m === improvedMsg) { + return ; + } + return null; +}; + +describe("test ErrorMsg", () => { + it("works without context for err", () => { + render(); + expect(screen.getByText(errMsg)).toBeInTheDocument(); + }); + + it("works without context for errString", () => { + render(); + expect(screen.getByText(errMsg)).toBeInTheDocument(); + }); + + it("renders the err if both err and errString provided", () => { + render(); + expect(screen.getByText(errMsg)).toBeInTheDocument(); + expect(screen.queryByText(improvedMsg)).not.toBeInTheDocument(); + }); + + it("improves err", () => { + render( + + + , + ); + expect(screen.getByText(improvedMsg)).toBeInTheDocument(); + }); + + it("improves errString", () => { + render( + + + , + ); + expect(screen.getByText(improvedMsg)).toBeInTheDocument(); + }); + + it("improves err for timeout", () => { + render( + + + , + ); + expect( + screen.getByText("Request timed out. Please try again."), + ).toBeInTheDocument(); + expect( + screen.queryByText("upstream request timeout"), + ).not.toBeInTheDocument(); + }); + + it("handles a different component that matches", () => { + render( + + + , + ); + const el = screen.getByText("Custom message"); + expect(el).toBeVisible(); + expect(screen.queryByText(improvedMsg)).not.toBeInTheDocument(); + expect(el).toHaveClass(/errorMsg/, /class-name/); + }); + + it("handles a different component that does not match", () => { + render( + + + , + ); + expect(screen.queryByText("Custom message")).not.toBeInTheDocument(); + expect(screen.getByText(errMsg)).toBeVisible(); + }); + + it("returns nothing if no err", () => { + render(); + expect(screen.queryByLabelText("error-msg")).not.toBeInTheDocument(); + }); + + it("returns nothing if no errString", () => { + render(); + expect(screen.queryByLabelText("error-msg")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index dfc3429d..6df6fd92 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -7,6 +7,11 @@ export { default as CharCount } from "./CharCount"; export { default as Checkbox } from "./Checkbox"; export { default as CodeBlock } from "./CodeBlock"; export { default as CommentForm } from "./CommentForm"; +export { default as ErrorMsg } from "./ErrorMsg"; +export { + default as ErrorMsgProvider, + isTimeoutError, +} from "./ErrorMsg/context"; export { default as ExternalLink } from "./ExternalLink"; export { default as FormInput } from "./FormInput"; export { default as FormSelect } from "./FormSelect"; From 616a30406314d9f764c2b2295e0c701043895954 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 3 Apr 2024 16:58:45 -0700 Subject: [PATCH 3/6] components: ButtonsWithError --- .../src/ButtonsWithError/index.module.css | 26 +++++++ .../components/src/ButtonsWithError/index.tsx | 58 ++++++++++++++++ .../__stories__/ButtonsWithError.stories.tsx | 59 ++++++++++++++++ .../src/__tests__/ButtonsWithError.test.tsx | 67 +++++++++++++++++++ packages/components/src/index.ts | 1 + 5 files changed, 211 insertions(+) create mode 100644 packages/components/src/ButtonsWithError/index.module.css create mode 100644 packages/components/src/ButtonsWithError/index.tsx create mode 100644 packages/components/src/__stories__/ButtonsWithError.stories.tsx create mode 100644 packages/components/src/__tests__/ButtonsWithError.test.tsx diff --git a/packages/components/src/ButtonsWithError/index.module.css b/packages/components/src/ButtonsWithError/index.module.css new file mode 100644 index 00000000..d97bcdd3 --- /dev/null +++ b/packages/components/src/ButtonsWithError/index.module.css @@ -0,0 +1,26 @@ +.stackedButton { + @apply flex flex-col; + button { + @apply mb-4 h-10; + } +} + +.error { + @apply max-w-lg text-center; +} + +.group { + @apply text-center mt-4; +} + +.cancel { + @apply px-2; +} + +.left { + @apply text-left ml-0; +} + +.right { + @apply text-right mr-0; +} diff --git a/packages/components/src/ButtonsWithError/index.tsx b/packages/components/src/ButtonsWithError/index.tsx new file mode 100644 index 00000000..2c3b2c72 --- /dev/null +++ b/packages/components/src/ButtonsWithError/index.tsx @@ -0,0 +1,58 @@ +import cx from "classnames"; +import React, { ReactNode } from "react"; +import Button from "../Button"; +import ErrorMsg from "../ErrorMsg"; +import css from "./index.module.css"; + +type Props = { + onCancel?: () => void; + error?: Error; + children: ReactNode; + className?: string; + left?: boolean; + stackedButton?: boolean; + right?: boolean; + ["data-cy"]?: string; +}; + +export default function ButtonsWithError({ + children, + onCancel, + error, + className, + left = false, + stackedButton = false, + right = false, + ...props +}: Props) { + return ( +
+ + {children} + {!!onCancel && ( + + cancel + + )} + + +
+ ); +} diff --git a/packages/components/src/__stories__/ButtonsWithError.stories.tsx b/packages/components/src/__stories__/ButtonsWithError.stories.tsx new file mode 100644 index 00000000..513ddba9 --- /dev/null +++ b/packages/components/src/__stories__/ButtonsWithError.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import Button from "../Button"; +import ButtonsWithError from "../ButtonsWithError"; + +const meta: Meta = { + title: "ButtonsWithError", + component: ButtonsWithError, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + children: , + onCancel: undefined, + }, +}; + +export const WithCancel: Story = { + args: { + ...Basic.args, + onCancel: () => {}, + }, +}; + +export const WithError: Story = { + args: { + ...WithCancel.args, + error: new Error("This is an error"), + }, +}; + +export const Stacked: Story = { + args: { + ...WithError.args, + stackedButton: true, + }, + parameters: { + viewport: { defaultViewport: "iphonex" }, + }, +}; + +export const Left: Story = { + args: { + ...WithError.args, + left: true, + }, +}; + +export const Right: Story = { + args: { + ...WithError.args, + right: true, + }, +}; diff --git a/packages/components/src/__tests__/ButtonsWithError.test.tsx b/packages/components/src/__tests__/ButtonsWithError.test.tsx new file mode 100644 index 00000000..69b2bb36 --- /dev/null +++ b/packages/components/src/__tests__/ButtonsWithError.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import Button from "../Button"; +import ButtonsWithError from "../ButtonsWithError"; +import { setup } from "./testUtils.test"; + +const errString = "This is the worst error"; + +describe("test ButtonsWithError", () => { + it("renders buttons without error", async () => { + const onCancel = jest.fn(); + const onClick = jest.fn(); + const { user } = setup( + + + , + ); + + const container = screen.getByLabelText("buttons-with-error"); + expect(container).toBeVisible(); + expect(container).toHaveClass("class-name"); + + const cancelButton = screen.getByText("cancel"); + expect(cancelButton).toBeVisible(); + expect(onCancel).not.toHaveBeenCalled(); + await user.click(cancelButton); + expect(onCancel).toHaveBeenCalled(); + + const testButton = screen.getByText("Test Button"); + expect(testButton).toBeVisible(); + expect(onClick).not.toHaveBeenCalled(); + await user.click(testButton); + expect(onClick).toHaveBeenCalled(); + }); + + it("renders buttons with error", () => { + render( + + + , + ); + + expect(screen.getByText("cancel")).toBeVisible(); + expect(screen.getByText("Test Button")).toBeVisible(); + + expect(screen.getByText(errString)).toBeVisible(); + }); + + it("renders buttons with error aligned left", () => { + render( + + + , + ); + + const buttonGroup = screen.getByLabelText("button-group"); + expect(buttonGroup).toBeVisible(); + expect(buttonGroup).toHaveClass(/left/); + + expect(screen.getByText("cancel")).toBeVisible(); + expect(screen.getByText("Test Button")).toBeVisible(); + + const err = screen.getByLabelText("error-msg"); + expect(err).toBeVisible(); + expect(err).toHaveClass(/left/); + }); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 6df6fd92..01d688eb 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -2,6 +2,7 @@ export { FilterOptionOption } from "react-select/dist/declarations/src/filters"; export { default as Btn } from "./Btn"; export { default as Button } from "./Button"; export { default as ButtonWithPopup } from "./ButtonWithPopup"; +export { default as ButtonsWithError } from "./ButtonsWithError"; export { default as CellDropdown } from "./CellDropdown"; export { default as CharCount } from "./CharCount"; export { default as Checkbox } from "./Checkbox"; From 4370d926f2df5bb105e88a603b210d5faac1acb7 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Thu, 4 Apr 2024 10:43:40 -0700 Subject: [PATCH 4/6] components: Add Modal: --- .../components/src/ErrorMsg/index.module.css | 2 +- .../components/src/Modal/index.module.css | 44 +++++++++ packages/components/src/Modal/index.tsx | 90 +++++++++++++++++++ .../src/__stories__/Modal.stories.tsx | 47 ++++++++++ .../components/src/__tests__/Modal.test.tsx | 44 +++++++++ packages/components/src/index.ts | 6 ++ 6 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 packages/components/src/Modal/index.module.css create mode 100644 packages/components/src/Modal/index.tsx create mode 100644 packages/components/src/__stories__/Modal.stories.tsx create mode 100644 packages/components/src/__tests__/Modal.test.tsx diff --git a/packages/components/src/ErrorMsg/index.module.css b/packages/components/src/ErrorMsg/index.module.css index 87c0438c..2525172d 100644 --- a/packages/components/src/ErrorMsg/index.module.css +++ b/packages/components/src/ErrorMsg/index.module.css @@ -1,3 +1,3 @@ .errorMsg { - @apply text-acc-red font-semibold mx-auto mt-6; + @apply text-acc-red font-semibold mx-auto pt-6; } diff --git a/packages/components/src/Modal/index.module.css b/packages/components/src/Modal/index.module.css new file mode 100644 index 00000000..62067a6c --- /dev/null +++ b/packages/components/src/Modal/index.module.css @@ -0,0 +1,44 @@ +.hide { + @apply hidden; +} + +.overlay { + @apply fixed top-0 left-0 w-full h-full bg-acc-darkgrey/70; + z-index: 1000; +} + +.modal { + @apply fixed bg-white h-auto top-1/2 left-1/2 rounded-lg -translate-x-1/2 -translate-y-1/2 w-[325px] md:w-[500px]; +} + +.top { + @apply flex justify-between items-center py-4 px-6 border-b; +} + +.inner { + @apply p-6 bg-ld-grey; + p { + @apply mb-6; + } + form { + @apply text-left; + } +} + +.buttons { + @apply flex justify-end items-center py-4 px-6 border-t; +} + +.close { + svg { + @apply text-lg; + } +} + +.error { + @apply pt-0 max-w-[275px] pr-4 ml-0 text-sm; +} + +.cancel { + @apply pr-4; +} diff --git a/packages/components/src/Modal/index.tsx b/packages/components/src/Modal/index.tsx new file mode 100644 index 00000000..3448f221 --- /dev/null +++ b/packages/components/src/Modal/index.tsx @@ -0,0 +1,90 @@ +import { useOnClickOutside } from "@dolthub/react-hooks"; +import { IoMdClose } from "@react-icons/all-files/io/IoMdClose"; +import cx from "classnames"; +import React, { ReactNode, useRef } from "react"; +import Btn from "../Btn"; +import Button from "../Button"; +import ErrorMsg from "../ErrorMsg"; +import css from "./index.module.css"; + +type ButtonProps = { + onRequestClose: () => void | Promise; + children?: ReactNode; + err?: Error; +}; + +type OuterProps = { + onRequestClose: () => void | Promise; + children: ReactNode; + title: string; + className?: string; + overlayClassName?: string; + isOpen: boolean; +}; + +type Props = OuterProps & { + button?: ReactNode; + err?: Error; +}; + +function Inner(props: Omit) { + const modalRef = useRef(null); + useOnClickOutside(modalRef, props.onRequestClose); + + return ( +
+
+
+

{props.title}

+ + + + + +
+
{props.children}
+
+
+ ); +} + +export function ModalOuter(props: OuterProps) { + if (!props.isOpen) return null; + return ; +} + +export function ModalInner(props: { children: ReactNode; className?: string }) { + return
{props.children}
; +} + +export function ModalButtons(props: ButtonProps) { + return ( +
+ + + + Cancel + + {props.children ?? } + +
+ ); +} + +export default function Modal({ children, ...props }: Props) { + if (!props.isOpen) return null; + return ( + + {children} + {props.button} + + ); +} diff --git a/packages/components/src/__stories__/Modal.stories.tsx b/packages/components/src/__stories__/Modal.stories.tsx new file mode 100644 index 00000000..b4ca06e6 --- /dev/null +++ b/packages/components/src/__stories__/Modal.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import Button from "../Button"; +import Modal from "../Modal"; + +const meta: Meta = { + title: "Modal", + component: Modal, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + title: "Modal", + isOpen: true, + onRequestClose: () => {}, + children:

Some information about this modal.

, + button: , + }, +}; + +export const WithError: Story = { + args: { + ...Basic.args, + err: new Error("An error occurred"), + }, +}; + +export const WithLongError: Story = { + args: { + ...Basic.args, + err: new Error( + "An error occurred that is much much longer and should go to the next line", + ), + }, +}; + +export const NoButton: Story = { + args: { + ...Basic.args, + button: undefined, + }, +}; diff --git a/packages/components/src/__tests__/Modal.test.tsx b/packages/components/src/__tests__/Modal.test.tsx new file mode 100644 index 00000000..87d0f8ee --- /dev/null +++ b/packages/components/src/__tests__/Modal.test.tsx @@ -0,0 +1,44 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import React from "react"; +import Modal from "../Modal"; + +describe("test Modal", () => { + it("renders a closed modal", () => { + const onRequestClose = jest.fn(); + render( + +
Inner component
+
, + ); + + expect(screen.queryByText("Modal")).not.toBeInTheDocument(); + expect(screen.queryByText("Inner component")).not.toBeInTheDocument(); + expect(screen.queryByLabelText("close")).not.toBeInTheDocument(); + }); + + it("renders an open modal", () => { + const onRequestClose = jest.fn(); + render( + +
Inner component
+
, + ); + + const modal = screen.getByRole("dialog"); + expect(modal).toBeVisible(); + expect(modal).toHaveClass("class-name"); + expect(screen.getByText("Modal")).toBeVisible(); + expect(screen.getByText("Inner component")).toBeVisible(); + + const close = screen.getByLabelText("close"); + expect(close).toBeVisible(); + expect(onRequestClose).toHaveBeenCalledTimes(0); + fireEvent.click(close); + expect(onRequestClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 01d688eb..aa76be25 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -20,6 +20,12 @@ export type * as FormSelectTypes from "./FormSelect/types"; export { default as Loader } from "./Loader"; export { default as Markdown } from "./Markdown"; export { default as MobileFormModal } from "./MobileFormModal"; +export { + default as Modal, + ModalButtons, + ModalInner, + ModalOuter, +} from "./Modal"; export { default as Navbar } from "./Navbar"; export { default as DesktopNavbar } from "./Navbar/ForDesktop"; export { default as MobileNavbar } from "./Navbar/ForMobile"; From 0e2fbedf86e384f54ae493d70a7f827ce9f37c07 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Thu, 4 Apr 2024 11:39:45 -0700 Subject: [PATCH 5/6] components: FormModal --- packages/components/package.json | 2 +- .../components/src/ButtonsWithError/index.tsx | 3 +- packages/components/src/Modal/ForForm.tsx | 38 +++++++++++++ packages/components/src/Modal/index.tsx | 2 +- .../src/__stories__/FormModal.stories.tsx | 28 ++++++++++ .../components/src/__tests__/Modal.test.tsx | 56 +++++++++++++++++++ packages/components/src/index.ts | 1 + 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 packages/components/src/Modal/ForForm.tsx create mode 100644 packages/components/src/__stories__/FormModal.stories.tsx diff --git a/packages/components/package.json b/packages/components/package.json index ad744422..f24c225e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -4,7 +4,7 @@ "name": "DoltHub" }, "description": "A collection of React components for common tasks", - "version": "0.1.12", + "version": "0.1.13", "repository": { "type": "git", "url": "git+https://github.com/dolthub/react-library.git" diff --git a/packages/components/src/ButtonsWithError/index.tsx b/packages/components/src/ButtonsWithError/index.tsx index 2c3b2c72..833b9cca 100644 --- a/packages/components/src/ButtonsWithError/index.tsx +++ b/packages/components/src/ButtonsWithError/index.tsx @@ -13,6 +13,7 @@ type Props = { stackedButton?: boolean; right?: boolean; ["data-cy"]?: string; + cancelText?: string; }; export default function ButtonsWithError({ @@ -42,7 +43,7 @@ export default function ButtonsWithError({ data-cy="cancel-button" className={css.cancel} > - cancel + {props.cancelText ?? "cancel"} )} diff --git a/packages/components/src/Modal/ForForm.tsx b/packages/components/src/Modal/ForForm.tsx new file mode 100644 index 00000000..76416609 --- /dev/null +++ b/packages/components/src/Modal/ForForm.tsx @@ -0,0 +1,38 @@ +import React, { SyntheticEvent } from "react"; +import { ModalButtons, ModalInner, ModalOuter, OuterProps } from "."; +import Button from "../Button"; + +type Props = OuterProps & { + onSubmit: (e: SyntheticEvent) => void | Promise; + err?: Error; + + // Button props + btnText: string; + buttonDataCy?: string; + pill?: boolean; + red?: boolean; + disabled?: boolean; + gradient?: boolean; +}; + +export default function FormModal({ children, ...props }: Props) { + return ( + +
+ {children} + + + +
+
+ ); +} diff --git a/packages/components/src/Modal/index.tsx b/packages/components/src/Modal/index.tsx index 3448f221..409387ed 100644 --- a/packages/components/src/Modal/index.tsx +++ b/packages/components/src/Modal/index.tsx @@ -13,7 +13,7 @@ type ButtonProps = { err?: Error; }; -type OuterProps = { +export type OuterProps = { onRequestClose: () => void | Promise; children: ReactNode; title: string; diff --git a/packages/components/src/__stories__/FormModal.stories.tsx b/packages/components/src/__stories__/FormModal.stories.tsx new file mode 100644 index 00000000..635dc629 --- /dev/null +++ b/packages/components/src/__stories__/FormModal.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import FormInput from "../FormInput"; +import FormModal from "../Modal/ForForm"; + +const meta: Meta = { + title: "FormModal", + component: FormModal, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + title: "Form Modal", + isOpen: true, + onRequestClose: () => {}, + children: ( +

+ +

+ ), + btnText: "Submit", + }, +}; diff --git a/packages/components/src/__tests__/Modal.test.tsx b/packages/components/src/__tests__/Modal.test.tsx index 87d0f8ee..fadfea92 100644 --- a/packages/components/src/__tests__/Modal.test.tsx +++ b/packages/components/src/__tests__/Modal.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import Modal from "../Modal"; +import FormModal from "../Modal/ForForm"; describe("test Modal", () => { it("renders a closed modal", () => { @@ -42,3 +43,58 @@ describe("test Modal", () => { expect(onRequestClose).toHaveBeenCalledTimes(1); }); }); + +describe("test FormModal", () => { + it("renders a closed modal", () => { + const onRequestClose = jest.fn(); + render( + +
Inner component
+
, + ); + + expect(screen.queryByText("Modal")).not.toBeInTheDocument(); + expect(screen.queryByText("Inner component")).not.toBeInTheDocument(); + expect(screen.queryByLabelText("close")).not.toBeInTheDocument(); + }); + + it("renders an open modal", () => { + const onRequestClose = jest.fn(); + const onSubmit = jest.fn(); + render( + +
Inner component
+
, + ); + + const modal = screen.getByRole("dialog"); + expect(modal).toBeVisible(); + expect(modal).toHaveClass("class-name"); + expect(screen.getByText("Modal")).toBeVisible(); + expect(screen.getByText("Inner component")).toBeVisible(); + + const close = screen.getByLabelText("close"); + expect(close).toBeVisible(); + expect(onRequestClose).toHaveBeenCalledTimes(0); + fireEvent.click(close); + expect(onRequestClose).toHaveBeenCalledTimes(1); + + const submit = screen.getByText("Submit"); + expect(submit).toBeVisible(); + fireEvent.click(submit); + expect(onSubmit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index aa76be25..052a2984 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -26,6 +26,7 @@ export { ModalInner, ModalOuter, } from "./Modal"; +export { default as FormModal } from "./Modal/ForForm"; export { default as Navbar } from "./Navbar"; export { default as DesktopNavbar } from "./Navbar/ForDesktop"; export { default as MobileNavbar } from "./Navbar/ForMobile"; From 134a12487ca138678f91daa706da09fb1c9f19a3 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Thu, 4 Apr 2024 11:43:25 -0700 Subject: [PATCH 6/6] components: Fixes --- packages/components/src/ErrorMsg/index.tsx | 14 -------------- .../src/__stories__/FormModal.stories.tsx | 4 ++-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/components/src/ErrorMsg/index.tsx b/packages/components/src/ErrorMsg/index.tsx index 18d94581..c2cd7d1f 100644 --- a/packages/components/src/ErrorMsg/index.tsx +++ b/packages/components/src/ErrorMsg/index.tsx @@ -35,17 +35,3 @@ export default function ErrorMsg({ err, errString, className }: Props) { ); } - -// function ConnectionLostMessage(props: { -// className?: string; -// errString: string; -// }) { -// return ( -//
-//
-// {props.errString} -//
-//
This error is often caused by a Dolt issue.
-//
-// ); -// } diff --git a/packages/components/src/__stories__/FormModal.stories.tsx b/packages/components/src/__stories__/FormModal.stories.tsx index 635dc629..0babdf9c 100644 --- a/packages/components/src/__stories__/FormModal.stories.tsx +++ b/packages/components/src/__stories__/FormModal.stories.tsx @@ -19,9 +19,9 @@ export const Basic: Story = { isOpen: true, onRequestClose: () => {}, children: ( -

+

-

+
), btnText: "Submit", },