-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #98 from dolthub/taylor/error-modal
components: ErrorMsg, ButtonsWithError, Modal
- Loading branch information
Showing
20 changed files
with
796 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
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; | ||
cancelText?: string; | ||
}; | ||
|
||
export default function ButtonsWithError({ | ||
children, | ||
onCancel, | ||
error, | ||
className, | ||
left = false, | ||
stackedButton = false, | ||
right = false, | ||
...props | ||
}: Props) { | ||
return ( | ||
<div className={className} aria-label="buttons-with-error"> | ||
<Button.Group | ||
className={cx(css.group, { | ||
[css.left]: left, | ||
[css.stackedButton]: stackedButton, | ||
[css.right]: right, | ||
})} | ||
data-cy={props["data-cy"]} | ||
> | ||
{children} | ||
{!!onCancel && ( | ||
<Button.Link | ||
onClick={onCancel} | ||
data-cy="cancel-button" | ||
className={css.cancel} | ||
> | ||
{props.cancelText ?? "cancel"} | ||
</Button.Link> | ||
)} | ||
</Button.Group> | ||
<ErrorMsg | ||
className={cx(css.error, { | ||
[css.left]: left, | ||
[css.right]: right, | ||
})} | ||
err={error} | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ErrorMsgContextType>({ | ||
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 ( | ||
<ErrorMsgContext.Provider value={value}> | ||
{children} | ||
</ErrorMsgContext.Provider> | ||
); | ||
} | ||
|
||
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") | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.errorMsg { | ||
@apply text-acc-red font-semibold mx-auto pt-6; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
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 ( | ||
<div className={cx(css.errorMsg, className)} aria-label="error-msg"> | ||
{splitMsg.map(m => ( | ||
<div key={m}>{m}</div> | ||
))} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>; | ||
err?: Error; | ||
|
||
// Button props | ||
btnText: string; | ||
buttonDataCy?: string; | ||
pill?: boolean; | ||
red?: boolean; | ||
disabled?: boolean; | ||
gradient?: boolean; | ||
}; | ||
|
||
export default function FormModal({ children, ...props }: Props) { | ||
return ( | ||
<ModalOuter {...props}> | ||
<form onSubmit={props.onSubmit}> | ||
<ModalInner>{children}</ModalInner> | ||
<ModalButtons onRequestClose={props.onRequestClose} err={props.err}> | ||
<Button | ||
type="submit" | ||
pill={props.pill} | ||
red={props.red} | ||
gradient={props.gradient} | ||
disabled={props.disabled} | ||
data-cy={props.buttonDataCy} | ||
> | ||
{props.btnText} | ||
</Button> | ||
</ModalButtons> | ||
</form> | ||
</ModalOuter> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>; | ||
children?: ReactNode; | ||
err?: Error; | ||
}; | ||
|
||
export type OuterProps = { | ||
onRequestClose: () => void | Promise<void>; | ||
children: ReactNode; | ||
title: string; | ||
className?: string; | ||
overlayClassName?: string; | ||
isOpen: boolean; | ||
}; | ||
|
||
type Props = OuterProps & { | ||
button?: ReactNode; | ||
err?: Error; | ||
}; | ||
|
||
function Inner(props: Omit<OuterProps, "isOpen">) { | ||
const modalRef = useRef<HTMLDivElement>(null); | ||
useOnClickOutside(modalRef, props.onRequestClose); | ||
|
||
return ( | ||
<div className={cx(css.overlay, props.overlayClassName)}> | ||
<div | ||
className={cx(css.modal, props.className)} | ||
ref={modalRef} | ||
role="dialog" | ||
> | ||
<div className={css.top}> | ||
<h3 data-cy="modal-title">{props.title}</h3> | ||
<Btn | ||
className={css.close} | ||
onClick={props.onRequestClose} | ||
data-cy="close-modal" | ||
> | ||
<span aria-label="close"> | ||
<IoMdClose /> | ||
</span> | ||
</Btn> | ||
</div> | ||
<div>{props.children}</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export function ModalOuter(props: OuterProps) { | ||
if (!props.isOpen) return null; | ||
return <Inner {...props} />; | ||
} | ||
|
||
export function ModalInner(props: { children: ReactNode; className?: string }) { | ||
return <div className={cx(css.inner, props.className)}>{props.children}</div>; | ||
} | ||
|
||
export function ModalButtons(props: ButtonProps) { | ||
return ( | ||
<div className={css.buttons} data-cy="modal-buttons"> | ||
<ErrorMsg className={css.error} err={props.err} /> | ||
<Button.Group> | ||
<Button.Link className={css.cancel} onClick={props.onRequestClose}> | ||
Cancel | ||
</Button.Link> | ||
{props.children ?? <Button onClick={props.onRequestClose}>OK</Button>} | ||
</Button.Group> | ||
</div> | ||
); | ||
} | ||
|
||
export default function Modal({ children, ...props }: Props) { | ||
if (!props.isOpen) return null; | ||
return ( | ||
<ModalOuter {...props}> | ||
<ModalInner>{children}</ModalInner> | ||
<ModalButtons {...props}>{props.button}</ModalButtons> | ||
</ModalOuter> | ||
); | ||
} |
Oops, something went wrong.