Remove unneeded files, more shadcn conversions

This commit is contained in:
2025-02-19 11:13:16 -05:00
parent e034e83198
commit ed62f03ba0
26 changed files with 63 additions and 3096 deletions

View File

@@ -1,41 +0,0 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
Button,
} from "@chakra-ui/react"
import { useRef } from "react"
import { useRsi } from "../../hooks/useRsi"
interface Props {
isOpen: boolean
onClose: () => void
onConfirm: () => void
}
export const ConfirmCloseAlert = ({ isOpen, onClose, onConfirm }: Props) => {
const { translations } = useRsi()
const cancelRef = useRef<HTMLButtonElement | null>(null)
return (
<AlertDialog isOpen={isOpen} onClose={onClose} leastDestructiveRef={cancelRef} isCentered id="rsi">
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader>{translations.alerts.confirmClose.headerTitle}</AlertDialogHeader>
<AlertDialogBody>{translations.alerts.confirmClose.bodyText}</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose} variant="secondary">
{translations.alerts.confirmClose.cancelButtonTitle}
</Button>
<Button colorScheme="red" onClick={onConfirm} ml={3}>
{translations.alerts.confirmClose.exitButtonTitle}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
)
}

View File

@@ -1,51 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { useRsi } from "../../hooks/useRsi"
interface Props {
isOpen: boolean
onClose: () => void
onConfirm: () => void
}
export const SubmitDataAlert = ({ isOpen, onClose, onConfirm }: Props) => {
const { allowInvalidSubmit, translations } = useRsi()
if (!isOpen) return null
return (
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{translations.alerts.submitIncomplete.headerTitle}
</AlertDialogTitle>
<AlertDialogDescription>
{allowInvalidSubmit
? translations.alerts.submitIncomplete.bodyText
: translations.alerts.submitIncomplete.bodyTextSubmitForbidden}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>
{translations.alerts.submitIncomplete.cancelButtonTitle}
</AlertDialogCancel>
{allowInvalidSubmit && (
<AlertDialogAction onClick={onConfirm}>
{translations.alerts.submitIncomplete.finishButtonTitle}
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -1,58 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { useRsi } from "../../hooks/useRsi"
interface Props {
isOpen: boolean
onClose: () => void
onConfirm: () => void
fields: string[]
}
export const UnmatchedFieldsAlert = ({ isOpen, onClose, onConfirm, fields }: Props) => {
const { allowInvalidSubmit, translations } = useRsi()
if (!isOpen) return null
return (
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{translations.alerts.unmatchedRequiredFields.headerTitle}
</AlertDialogTitle>
<div className="space-y-3">
<AlertDialogDescription>
{translations.alerts.unmatchedRequiredFields.bodyText}
</AlertDialogDescription>
<p className="text-sm text-muted-foreground">
{translations.alerts.unmatchedRequiredFields.listTitle}{" "}
<span className="font-bold">
{fields.join(", ")}
</span>
</p>
</div>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>
{translations.alerts.unmatchedRequiredFields.cancelButtonTitle}
</AlertDialogCancel>
{allowInvalidSubmit && (
<AlertDialogAction onClick={onConfirm}>
{translations.alerts.unmatchedRequiredFields.continueButtonTitle}
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -1,33 +0,0 @@
import { Button, ModalFooter, useStyleConfig } from "@chakra-ui/react"
import { themeOverrides } from "../theme"
type ContinueButtonProps = {
onContinue: (val: any) => void
onBack?: () => void
title: string
backTitle?: string
isLoading?: boolean
}
export const ContinueButton = ({ onContinue, onBack, title, backTitle, isLoading }: ContinueButtonProps) => {
const styles = useStyleConfig("Modal") as (typeof themeOverrides)["components"]["Modal"]["baseStyle"]
const nextButtonMobileWidth = onBack ? "8rem" : "100%"
return (
<ModalFooter>
{onBack && (
<Button size="md" sx={styles.backButton} onClick={onBack} isLoading={isLoading} variant="link">
{backTitle}
</Button>
)}
<Button
size="lg"
w={{ base: nextButtonMobileWidth, md: "21rem" }}
sx={styles.continueButton}
onClick={onContinue}
isLoading={isLoading}
>
{title}
</Button>
</ModalFooter>
)
}

View File

@@ -1,25 +0,0 @@
import { Box } from "@chakra-ui/react"
type FadingWrapperProps = {
gridColumn: string
gridRow: string
}
export const FadingWrapper = ({ gridColumn, gridRow }: FadingWrapperProps) => (
<>
<Box
gridColumn={gridColumn}
gridRow={gridRow}
borderRadius="1.2rem"
border="1px solid"
borderColor="border"
pointerEvents="none"
/>
<Box
gridColumn={gridColumn}
gridRow={gridRow}
pointerEvents="none"
bgGradient="linear(to bottom, backgroundAlpha, background)"
/>
</>
)

View File

@@ -1,30 +0,0 @@
import { Select } from "chakra-react-select"
import type { SelectOption } from "../../types"
import { customComponents } from "./MenuPortal"
import { useStyleConfig } from "@chakra-ui/react"
import type { Styles } from "../../steps/MatchColumnsStep/components/ColumnGrid"
interface Props {
onChange: (value: SelectOption | null) => void
value?: SelectOption
options: readonly SelectOption[]
placeholder?: string
name?: string
}
export const MatchColumnSelect = ({ onChange, value, options, placeholder, name }: Props) => {
const styles = useStyleConfig("MatchColumnsStep") as Styles
return (
<Select<SelectOption, false>
value={value || null}
colorScheme="gray"
useBasicStyles
onChange={onChange}
placeholder={placeholder}
options={options}
chakraStyles={styles.select}
menuPosition="fixed"
components={customComponents}
aria-label={name}
/>
)
}

View File

@@ -1,75 +0,0 @@
import React, { useEffect, useLayoutEffect, useState } from "react"
import ReactDOM from "react-dom"
import { Box, useTheme } from "@chakra-ui/react"
import { usePopper } from "@chakra-ui/popper"
import { rootId } from "../Providers"
import { useRsi } from "../../hooks/useRsi"
function createWrapperAndAppendToBody(wrapperId: string) {
const wrapperElement = document.createElement("div")
wrapperElement.setAttribute("id", wrapperId)
document.body.appendChild(wrapperElement)
return wrapperElement
}
export const SELECT_DROPDOWN_ID = "react-select-dropdown-wrapper"
interface PortalProps {
controlElement: HTMLDivElement | null
children: React.ReactNode
}
const MenuPortal = (props: PortalProps) => {
const theme = useTheme()
const { rtl } = useRsi()
const { popperRef, referenceRef } = usePopper({
strategy: "fixed",
matchWidth: true,
})
const [wrapperElement, setWrapperElement] = useState<HTMLElement | null>(null)
useLayoutEffect(() => {
let element = document.getElementById(SELECT_DROPDOWN_ID)
let systemCreated = false
if (!element) {
systemCreated = true
element = createWrapperAndAppendToBody(SELECT_DROPDOWN_ID)
}
setWrapperElement(element)
return () => {
if (systemCreated && element?.parentNode) {
element.parentNode.removeChild(element)
}
}
}, [])
useEffect(() => {
referenceRef(props.controlElement)
}, [props.controlElement, referenceRef])
// wrapperElement state will be null on very first render.
if (wrapperElement === null) return null
return ReactDOM.createPortal(
<Box
dir={rtl ? "rtl" : "ltr"}
ref={popperRef}
zIndex={theme.zIndices.tooltip}
sx={{
"&[data-popper-reference-hidden]": {
visibility: "hidden",
pointerEvents: "none",
},
}}
id={rootId}
>
{props.children}
</Box>,
wrapperElement,
)
}
export const customComponents = {
MenuPortal,
}

View File

@@ -1,33 +0,0 @@
import { rootId } from "../Providers"
import { Select } from "chakra-react-select"
import type { SelectOption } from "../../types"
import { useStyleConfig } from "@chakra-ui/react"
import type { themeOverrides } from "../../theme"
interface Props {
onChange: (value: SelectOption | null) => void
value?: SelectOption
options: readonly SelectOption[]
}
export const TableSelect = ({ onChange, value, options }: Props) => {
const styles = useStyleConfig(
"ValidationStep",
) as (typeof themeOverrides)["components"]["ValidationStep"]["baseStyle"]
return (
<Select<SelectOption, false>
autoFocus
useBasicStyles
size="sm"
value={value}
onChange={onChange}
placeholder=" "
closeMenuOnScroll
menuPosition="fixed"
menuIsOpen
menuPortalTarget={document.getElementById(rootId)}
options={options}
chakraStyles={styles.select}
/>
)
}

View File

@@ -4,9 +4,6 @@ import { ColumnType } from "../MatchColumnsStep"
import { useRsi } from "../../../hooks/useRsi" import { useRsi } from "../../../hooks/useRsi"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import { Box } from "@/components/ui/box"
import { Text } from "@/components/ui/text"
import { FadingWrapper } from "@/components/ui/fading-wrapper"
type ColumnGridProps<T extends string> = { type ColumnGridProps<T extends string> = {
columns: Columns<T> columns: Columns<T>

View File

@@ -1,32 +0,0 @@
import { Box, Text, useStyleConfig } from "@chakra-ui/react"
import { MatchColumnSelect } from "../../../components/Selects/MatchColumnSelect"
import { getFieldOptions } from "../utils/getFieldOptions"
import { useRsi } from "../../../hooks/useRsi"
import type { MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn } from "../MatchColumnsStep"
import type { Styles } from "./ColumnGrid"
interface Props<T> {
option: MatchedOptions<T> | Partial<MatchedOptions<T>>
column: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>
onSubChange: (val: T, index: number, option: string) => void
}
export const SubMatchingSelect = <T extends string>({ option, column, onSubChange }: Props<T>) => {
const styles = useStyleConfig("MatchColumnsStep") as Styles
const { translations, fields } = useRsi<T>()
const options = getFieldOptions(fields, column.value)
const value = options.find((opt) => opt.value == option.value)
return (
<Box pl={2} pb="0.375rem">
<Text sx={styles.selectColumn.selectLabel}>{option.entry}</Text>
<MatchColumnSelect
value={value}
placeholder={translations.matchColumnsStep.subSelectPlaceholder}
onChange={(value) => onSubChange(value?.value as T, column.index, option.entry!)}
options={options}
name={option.entry}
/>
</Box>
)
}

View File

@@ -1,33 +0,0 @@
import { defaultTheme } from "../../../ReactSpreadsheetImport"
import { MatchColumnsStep } from "../MatchColumnsStep"
import { Providers } from "../../../components/Providers"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { ModalWrapper } from "../../../components/ModalWrapper"
export default {
title: "Match Columns Steps",
parameters: {
layout: "fullscreen",
},
}
const mockData = [
["id", "first_name", "last_name", "email", "gender", "ip_address"],
["2", "Geno", "Gencke", "ggencke0@tinypic.com", "Female", "17.204.180.40"],
["3", "Bertram", "Twyford", "btwyford1@seattletimes.com", "Genderqueer", "188.98.2.13"],
["4", "Tersina", "Isacke", "tisacke2@edublogs.org", "Non-binary", "237.69.180.31"],
["5", "Yoko", "Guilliland", "yguilliland3@elegantthemes.com", "Male", "179.123.237.119"],
["6", "Freida", "Fearns", "ffearns4@fotki.com", "Male", "184.48.15.1"],
["7", "Mildrid", "Mount", "mmount5@last.fm", "Male", "26.97.160.103"],
["8", "Jolene", "Darlington", "jdarlington6@jalbum.net", "Agender", "172.14.232.84"],
["9", "Craig", "Dickie", "cdickie7@virginia.edu", "Male", "143.248.220.47"],
["10", "Jere", "Shier", "jshier8@comcast.net", "Agender", "10.143.62.161"],
]
export const Basic = () => (
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={mockData[0] as string[]} data={mockData.slice(1)} onContinue={() => {}} />
</ModalWrapper>
</Providers>
)

View File

@@ -1,870 +0,0 @@
import "@testing-library/jest-dom"
import { render, waitFor, screen } from "@testing-library/react"
import { MatchColumnsStep } from "../MatchColumnsStep"
import { defaultTheme, ReactSpreadsheetImport } from "../../../ReactSpreadsheetImport"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
import userEvent from "@testing-library/user-event"
import type { Fields } from "../../../types"
import selectEvent from "react-select-event"
import { translations } from "../../../translationsRSIProps"
import { SELECT_DROPDOWN_ID } from "../../../components/Selects/MenuPortal"
import { StepType } from "../../UploadFlow"
const fields: Fields<any> = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
example: "Stephanie",
},
{
label: "Mobile Phone",
key: "mobile",
fieldType: {
type: "input",
},
example: "+12323423",
},
{
label: "Is cool",
key: "is_cool",
fieldType: {
type: "checkbox",
},
example: "No",
},
]
const CONTINUE_BUTTON = "Next"
const MUTATED_ENTRY = "mutated entry"
const ERROR_MESSAGE = "Something happened"
describe("Match Columns automatic matching", () => {
test("AutoMatch column and click next", async () => {
const header = ["namezz", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
// finds only names with automatic matching
const result = [{ name: data[0][0] }, { name: data[1][0] }, { name: data[2][0] }]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("AutoMatching disabled does not match any columns", async () => {
const header = ["Name", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
// finds only names with automatic matching
const result = [{}, {}, {}]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields, autoMapHeaders: false }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("AutoMatching exact values", async () => {
const header = ["Name", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
// finds only names with automatic matching
const result = [{ name: data[0][0] }, { name: data[1][0] }, { name: data[2][0] }]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields, autoMapDistance: 1 }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("AutoMatches only one value", async () => {
const header = ["first name", "name", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
// finds only names with automatic matching
const result = [{ name: data[0][1] }, { name: data[1][1] }, { name: data[2][1] }]
const alternativeFields = [
{
label: "Name",
key: "name",
alternateMatches: ["first name"],
fieldType: {
type: "input",
},
example: "Stephanie",
},
] as const
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields: alternativeFields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("AutoMatches select values on mount", async () => {
const header = ["first name", "count", "Email"]
const OPTION_RESULT_ONE = "John"
const OPTION_RESULT_ONE_VALUE = "1"
const OPTION_RESULT_TWO = "Dane"
const OPTION_RESULT_TWO_VALUE = "2"
const OPTION_RESULT_THREE = "Kane"
const data = [
// match by option label
[OPTION_RESULT_ONE, "123", "j@j.com"],
// match by option value
[OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"],
// do not match
[OPTION_RESULT_THREE, "534", "kane@linch.com"],
]
const options = [
{ label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE },
{ label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE },
]
// finds only names with automatic matching
const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }]
const alternativeFields = [
{
label: "Name",
key: "name",
alternateMatches: ["first name"],
fieldType: {
type: "select",
options,
},
example: "Stephanie",
},
] as const
const onContinue = jest.fn()
render(
<Providers
theme={defaultTheme}
rsiValues={{ ...mockRsiValues, fields: alternativeFields, autoMapSelectValues: true }}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument()
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("Does not auto match select values when autoMapSelectValues:false", async () => {
const header = ["first name", "count", "Email"]
const OPTION_RESULT_ONE = "John"
const OPTION_RESULT_ONE_VALUE = "1"
const OPTION_RESULT_TWO = "Dane"
const OPTION_RESULT_TWO_VALUE = "2"
const OPTION_RESULT_THREE = "Kane"
const data = [
// match by option label
[OPTION_RESULT_ONE, "123", "j@j.com"],
// match by option value
[OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"],
// do not match
[OPTION_RESULT_THREE, "534", "kane@linch.com"],
]
const options = [
{ label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE },
{ label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE },
]
const result = [{ name: undefined }, { name: undefined }, { name: undefined }]
const alternativeFields = [
{
label: "Name",
key: "name",
alternateMatches: ["first name"],
fieldType: {
type: "select",
options,
},
example: "Stephanie",
},
] as const
const onContinue = jest.fn()
render(
<Providers
theme={defaultTheme}
rsiValues={{ ...mockRsiValues, fields: alternativeFields, autoMapSelectValues: false }}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
expect(screen.getByText(/3 Unmatched/)).toBeInTheDocument()
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("AutoMatches select values on select", async () => {
const header = ["first name", "count", "Email"]
const OPTION_RESULT_ONE = "John"
const OPTION_RESULT_ONE_VALUE = "1"
const OPTION_RESULT_TWO = "Dane"
const OPTION_RESULT_TWO_VALUE = "2"
const OPTION_RESULT_THREE = "Kane"
const data = [
// match by option label
[OPTION_RESULT_ONE, "123", "j@j.com"],
// match by option value
[OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"],
// do not match
[OPTION_RESULT_THREE, "534", "kane@linch.com"],
]
const options = [
{ label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE },
{ label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE },
]
// finds only names with automatic matching
const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }]
const alternativeFields = [
{
label: "Name",
key: "name",
fieldType: {
type: "select",
options,
},
example: "Stephanie",
},
] as const
const onContinue = jest.fn()
render(
<Providers
theme={defaultTheme}
rsiValues={{ ...mockRsiValues, fields: alternativeFields, autoMapSelectValues: true }}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
<div id={SELECT_DROPDOWN_ID} />
</ModalWrapper>
</Providers>,
)
await selectEvent.select(screen.getByLabelText(header[0]), alternativeFields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument()
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("Boolean-like values are returned as Booleans", async () => {
const header = ["namezz", "is_cool", "Email"]
const data = [
["John", "yes", "j@j.com"],
["Dane", "TRUE", "dane@bane.com"],
["Kane", "false", "kane@linch.com"],
["Kaney", "no", "kane@linch.com"],
["Kanye", "maybe", "kane@linch.com"],
]
const result = [
{ name: data[0][0], is_cool: true },
{ name: data[1][0], is_cool: true },
{ name: data[2][0], is_cool: false },
{ name: data[3][0], is_cool: false },
{ name: data[4][0], is_cool: false },
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("Boolean-like values are returned as Booleans for 'booleanMatches' props", async () => {
const BOOLEAN_MATCHES_VALUE = "definitely"
const header = ["is_cool"]
const data = [["true"], ["false"], [BOOLEAN_MATCHES_VALUE]]
const fields = [
{
label: "Is cool",
key: "is_cool",
fieldType: {
type: "checkbox",
booleanMatches: { [BOOLEAN_MATCHES_VALUE]: true },
},
example: "No",
},
] as const
const result = [{ is_cool: true }, { is_cool: false }, { is_cool: true }]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
})
describe("Match Columns general tests", () => {
test("Displays all user header columns", async () => {
const header = ["namezz", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
expect(screen.getByText(header[0])).toBeInTheDocument()
expect(screen.getByText(header[1])).toBeInTheDocument()
expect(screen.getByText(header[2])).toBeInTheDocument()
})
test("Displays two rows of example data", async () => {
const header = ["namezz", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
// only displays two rows
expect(screen.queryByText(data[0][0])).toBeInTheDocument()
expect(screen.queryByText(data[0][1])).toBeInTheDocument()
expect(screen.queryByText(data[0][2])).toBeInTheDocument()
expect(screen.queryByText(data[1][0])).toBeInTheDocument()
expect(screen.queryByText(data[1][1])).toBeInTheDocument()
expect(screen.queryByText(data[1][2])).toBeInTheDocument()
expect(screen.queryByText(data[2][0])).not.toBeInTheDocument()
expect(screen.queryByText(data[2][1])).not.toBeInTheDocument()
expect(screen.queryByText(data[2][2])).not.toBeInTheDocument()
})
test("Displays all fields in selects dropdown", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const firstSelect = screen.getByLabelText(header[0])
await userEvent.click(firstSelect)
fields.forEach((field) => {
expect(screen.queryByText(field.label)).toBeInTheDocument()
})
})
test("Manually matches first column", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const result = [{ name: data[0][0] }, { name: data[1][0] }, { name: data[2][0] }]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
<div id={SELECT_DROPDOWN_ID} />
</ModalWrapper>
</Providers>,
)
await selectEvent.select(screen.getByLabelText(header[0]), fields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("Checkmark changes when field is matched", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
<div id={SELECT_DROPDOWN_ID} />
</ModalWrapper>
</Providers>,
)
const checkmark = screen.getAllByTestId("column-checkmark")[0]
// kinda dumb way to check if it has checkmark or not
expect(checkmark).toBeEmptyDOMElement()
await selectEvent.select(screen.getByLabelText(header[0]), fields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
expect(checkmark).not.toBeEmptyDOMElement()
})
test("Selecting select field adds more selects", async () => {
const OPTION_ONE = "one"
const OPTION_TWO = "two"
const OPTION_RESULT_ONE = "uno"
const OPTION_RESULT_TWO = "dos"
const options = [
{ label: "One", value: OPTION_RESULT_ONE },
{ label: "Two", value: OPTION_RESULT_TWO },
]
const header = ["Something random"]
const data = [[OPTION_ONE], [OPTION_TWO], [OPTION_ONE]]
const result = [
{
team: OPTION_RESULT_ONE,
},
{
team: OPTION_RESULT_TWO,
},
{
team: OPTION_RESULT_ONE,
},
]
const enumFields = [
{
label: "Team",
key: "team",
fieldType: {
type: "select",
options: options,
},
},
] as const
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields: enumFields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
<div id={SELECT_DROPDOWN_ID} />
</ModalWrapper>
</Providers>,
)
expect(screen.queryByTestId("accordion-button")).not.toBeInTheDocument()
await selectEvent.select(screen.getByLabelText(header[0]), enumFields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
expect(screen.queryByTestId("accordion-button")).toBeInTheDocument()
await userEvent.click(screen.getByTestId("accordion-button"))
await selectEvent.select(screen.getByLabelText(data[0][0]), options[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
await selectEvent.select(screen.getByLabelText(data[1][0]), options[1].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(result)
})
test("Can ignore columns", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const ignoreButton = screen.getAllByLabelText("Ignore column")[0]
expect(screen.queryByText(translations.matchColumnsStep.ignoredColumnText)).not.toBeInTheDocument()
await userEvent.click(ignoreButton)
expect(screen.queryByText(translations.matchColumnsStep.ignoredColumnText)).toBeInTheDocument()
})
test("Required unselected fields show warning alert on submit", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const requiredFields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
example: "Stephanie",
validations: [
{
rule: "required",
errorMessage: "Hello",
},
],
},
] as const
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields: requiredFields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
expect(onContinue).not.toBeCalled()
expect(screen.queryByText(translations.alerts.unmatchedRequiredFields.bodyText)).toBeInTheDocument()
const continueButton = screen.getByRole("button", {
name: "Continue",
})
await userEvent.click(continueButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
})
test("Selecting the same field twice shows toast", async () => {
const header = ["Something random", "Phone", "Email"]
const data = [
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
["Kane", "534", "kane@linch.com"],
]
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockRsiValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<MatchColumnsStep headerValues={header} data={data} onContinue={onContinue} />
<div id={SELECT_DROPDOWN_ID} />
</ModalWrapper>
</Providers>,
)
await selectEvent.select(screen.getByLabelText(header[0]), fields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
await selectEvent.select(screen.getByLabelText(header[1]), fields[0].label, {
container: document.getElementById(SELECT_DROPDOWN_ID)!,
})
const toasts = await screen.queryAllByText(translations.matchColumnsStep.duplicateColumnWarningDescription)
expect(toasts?.[0]).toBeInTheDocument()
})
test("matchColumnsStepHook should be called after columns are matched", async () => {
const matchColumnsStepHook = jest.fn(async (values) => values)
const mockValues = {
...mockRsiValues,
fields: mockRsiValues.fields.filter((field) => field.key === "name" || field.key === "age"),
}
render(
<ReactSpreadsheetImport
{...mockValues}
matchColumnsStepHook={matchColumnsStepHook}
initialStepState={{
type: StepType.matchColumns,
data: [
["Josh", "2"],
["Charlie", "3"],
["Lena", "50"],
],
headerValues: ["name", "age"],
}}
/>,
)
const continueButton = screen.getByText(CONTINUE_BUTTON)
await userEvent.click(continueButton)
await waitFor(() => {
expect(matchColumnsStepHook).toBeCalled()
})
})
test("matchColumnsStepHook mutations to rawData should show up in ValidationStep", async () => {
const matchColumnsStepHook = jest.fn(async ([firstEntry, ...values]) => {
return [{ ...firstEntry, name: MUTATED_ENTRY }, ...values]
})
const mockValues = {
...mockRsiValues,
fields: mockRsiValues.fields.filter((field) => field.key === "name" || field.key === "age"),
}
render(
<ReactSpreadsheetImport
{...mockValues}
matchColumnsStepHook={matchColumnsStepHook}
initialStepState={{
type: StepType.matchColumns,
data: [
["Josh", "2"],
["Charlie", "3"],
["Lena", "50"],
],
headerValues: ["name", "age"],
}}
/>,
)
const continueButton = screen.getByText(CONTINUE_BUTTON)
await userEvent.click(continueButton)
const mutatedEntry = await screen.findByText(MUTATED_ENTRY)
expect(mutatedEntry).toBeInTheDocument()
})
test("Should show error toast if error is thrown in matchColumnsStepHook", async () => {
const matchColumnsStepHook = jest.fn(async () => {
throw new Error(ERROR_MESSAGE)
return undefined as any
})
const mockValues = {
...mockRsiValues,
fields: mockRsiValues.fields.filter((field) => field.key === "name" || field.key === "age"),
}
render(
<ReactSpreadsheetImport
{...mockValues}
matchColumnsStepHook={matchColumnsStepHook}
initialStepState={{
type: StepType.matchColumns,
data: [
["Josh", "2"],
["Charlie", "3"],
["Lena", "50"],
],
headerValues: ["name", "age"],
}}
/>,
)
const continueButton = screen.getByText(CONTINUE_BUTTON)
await userEvent.click(continueButton)
const errorToast = await screen.findAllByText(ERROR_MESSAGE, undefined, { timeout: 5000 })
expect(errorToast?.[0]).toBeInTheDocument()
})
})

View File

@@ -1,19 +0,0 @@
import { headerSelectionTableFields, mockRsiValues } from "../../../stories/mockRsiValues"
import { SelectHeaderStep } from "../SelectHeaderStep"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
import { defaultTheme } from "../../../ReactSpreadsheetImport"
export default {
title: "Select Header Step",
parameters: {
layout: "fullscreen",
},
}
export const Basic = () => (
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<SelectHeaderStep data={headerSelectionTableFields} onContinue={async () => {}} />
</ModalWrapper>
</Providers>
)

View File

@@ -1,217 +0,0 @@
import "@testing-library/jest-dom"
import { render, waitFor, screen, fireEvent } from "@testing-library/react"
import { SelectHeaderStep } from "../SelectHeaderStep"
import { defaultTheme, ReactSpreadsheetImport } from "../../../ReactSpreadsheetImport"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
import userEvent from "@testing-library/user-event"
import { readFileSync } from "fs"
import { StepType } from "../../UploadFlow"
const MUTATED_HEADER = "mutated header"
const CONTINUE_BUTTON = "Next"
const ERROR_MESSAGE = "Something happened"
const RAW_DATE = "2020-03-03"
const FORMATTED_DATE = "2020/03/03"
const TRAILING_CELL = "trailingcell"
describe("Select header step tests", () => {
test("Select header row and click next", async () => {
const data = [
["Some random header"],
["2030"],
["Name", "Phone", "Email"],
["John", "123", "j@j.com"],
["Dane", "333", "dane@bane.com"],
]
const selectRowIndex = 2
const onContinue = jest.fn()
const onBack = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<SelectHeaderStep data={data} onContinue={onContinue} onBack={onBack} />
</ModalWrapper>
</Providers>,
)
const radioButtons = screen.getAllByRole("radio")
await userEvent.click(radioButtons[selectRowIndex])
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(data[selectRowIndex])
expect(onContinue.mock.calls[0][1]).toEqual(data.slice(selectRowIndex + 1))
})
test("selectHeaderStepHook should be called after header is selected", async () => {
const selectHeaderStepHook = jest.fn(async (headerValues, data) => {
return { headerValues, data }
})
render(<ReactSpreadsheetImport {...mockRsiValues} selectHeaderStepHook={selectHeaderStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/Workbook2.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const continueButton = await screen.findByText(CONTINUE_BUTTON, undefined, { timeout: 10000 })
fireEvent.click(continueButton)
await waitFor(() => {
expect(selectHeaderStepHook).toBeCalledWith(
["name", "age", "date"],
[
["Josh", "2", "2020-03-03"],
["Charlie", "3", "2010-04-04"],
["Lena", "50", "1994-02-27"],
],
)
})
})
test("selectHeaderStepHook should be able to modify raw data", async () => {
const selectHeaderStepHook = jest.fn(async ([val, ...headerValues], data) => {
return { headerValues: [MUTATED_HEADER, ...headerValues], data }
})
render(
<ReactSpreadsheetImport
{...mockRsiValues}
selectHeaderStepHook={selectHeaderStepHook}
initialStepState={{
type: StepType.selectHeader,
data: [
["name", "age"],
["Josh", "2"],
["Charlie", "3"],
["Lena", "50"],
],
}}
/>,
)
const continueButton = screen.getByText(CONTINUE_BUTTON)
fireEvent.click(continueButton)
const mutatedHeader = await screen.findByText(MUTATED_HEADER)
await waitFor(() => {
expect(mutatedHeader).toBeInTheDocument()
})
})
test("Should show error toast if error is thrown in selectHeaderStepHook", async () => {
const selectHeaderStepHook = jest.fn(async () => {
throw new Error(ERROR_MESSAGE)
return undefined as any
})
render(
<ReactSpreadsheetImport
{...mockRsiValues}
selectHeaderStepHook={selectHeaderStepHook}
initialStepState={{
type: StepType.selectHeader,
data: [
["name", "age"],
["Josh", "2"],
["Charlie", "3"],
["Lena", "50"],
],
}}
/>,
)
const continueButton = screen.getByText(CONTINUE_BUTTON)
await userEvent.click(continueButton)
const errorToast = await screen.findAllByText(ERROR_MESSAGE, undefined, { timeout: 5000 })
expect(errorToast?.[0]).toBeInTheDocument()
})
test("dateFormat property should NOT be applied to dates read from csv files IF parseRaw=true", async () => {
const file = new File([RAW_DATE], "test.csv", {
type: "text/csv",
})
render(<ReactSpreadsheetImport {...mockRsiValues} dateFormat="yyyy/mm/dd" parseRaw={true} />)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
const el = await screen.findByText(RAW_DATE, undefined, { timeout: 5000 })
expect(el).toBeInTheDocument()
})
test("dateFormat property should be applied to dates read from csv files IF parseRaw=false", async () => {
const file = new File([RAW_DATE], "test.csv", {
type: "text/csv",
})
render(<ReactSpreadsheetImport {...mockRsiValues} dateFormat="yyyy/mm/dd" parseRaw={false} />)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
const el = await screen.findByText(FORMATTED_DATE, undefined, { timeout: 5000 })
expect(el).toBeInTheDocument()
})
test("dateFormat property should be applied to dates read from xlsx files", async () => {
render(<ReactSpreadsheetImport {...mockRsiValues} dateFormat="yyyy/mm/dd" />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/Workbook2.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const el = await screen.findByText(FORMATTED_DATE, undefined, { timeout: 10000 })
expect(el).toBeInTheDocument()
})
test.skip(
"trailing (not under a header) cells should be rendered in SelectHeaderStep table, " +
"but not in MatchColumnStep if a shorter row is selected as a header",
async () => {
const selectHeaderStepHook = jest.fn(async (headerValues, data) => {
return { headerValues, data }
})
render(<ReactSpreadsheetImport {...mockRsiValues} selectHeaderStepHook={selectHeaderStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/TrailingCellsWorkbook.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const trailingCell = await screen.findByText(TRAILING_CELL, undefined, { timeout: 10000 })
expect(trailingCell).toBeInTheDocument()
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
const trailingCellNextPage = await screen.findByText(TRAILING_CELL, undefined, { timeout: 10000 })
expect(trailingCellNextPage).not.toBeInTheDocument()
},
)
})

View File

@@ -1,22 +0,0 @@
import { defaultTheme } from "../../../ReactSpreadsheetImport"
import { SelectSheetStep } from "../SelectSheetStep"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
export default {
title: "Select Sheet Step",
parameters: {
layout: "fullscreen",
},
}
const sheetNames = ["Sheet1", "Sheet2", "Sheet3"]
export const Basic = () => (
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<SelectSheetStep sheetNames={sheetNames} onContinue={async () => {}} />
</ModalWrapper>
</Providers>
)

View File

@@ -1,118 +0,0 @@
import "@testing-library/jest-dom"
import { render, waitFor, screen, fireEvent, act } from "@testing-library/react"
import { SelectSheetStep } from "../SelectSheetStep"
import { defaultTheme, ReactSpreadsheetImport } from "../../../ReactSpreadsheetImport"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
import userEvent from "@testing-library/user-event"
import { readFileSync } from "fs"
const SHEET_TITLE_1 = "Sheet1"
const SHEET_TITLE_2 = "Sheet2"
const SELECT_HEADER_TABLE_ENTRY_1 = "Charlie"
const SELECT_HEADER_TABLE_ENTRY_2 = "Josh"
const SELECT_HEADER_TABLE_ENTRY_3 = "50"
const ERROR_MESSAGE = "Something happened"
test("Should render select sheet screen if multi-sheet excel file was uploaded", async () => {
render(<ReactSpreadsheetImport {...mockRsiValues} />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/Workbook1.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const sheetTitle = await screen.findByText(SHEET_TITLE_1, undefined, { timeout: 5000 })
const sheetTitle2 = screen.getByRole("radio", { name: SHEET_TITLE_2 })
expect(sheetTitle).toBeInTheDocument()
expect(sheetTitle2).toBeInTheDocument()
})
test("Should render select header screen with relevant data if single-sheet excel file was uploaded", async () => {
render(<ReactSpreadsheetImport {...mockRsiValues} />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/Workbook2.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const tableEntry1 = await screen.findByText(SELECT_HEADER_TABLE_ENTRY_1, undefined, { timeout: 5000 })
const tableEntry2 = screen.getByRole("gridcell", { name: SELECT_HEADER_TABLE_ENTRY_2 })
const tableEntry3 = screen.getByRole("gridcell", { name: SELECT_HEADER_TABLE_ENTRY_3 })
expect(tableEntry1).toBeInTheDocument()
expect(tableEntry2).toBeInTheDocument()
expect(tableEntry3).toBeInTheDocument()
})
test("Select sheet and click next", async () => {
const sheetNames = ["Sheet1", "Sheet2"]
const selectSheetIndex = 1
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<SelectSheetStep sheetNames={sheetNames} onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const firstRadio = screen.getByLabelText(sheetNames[selectSheetIndex])
await userEvent.click(firstRadio)
const nextButton = screen.getByRole("button", {
name: "Next",
})
await userEvent.click(nextButton)
await waitFor(() => {
expect(onContinue).toBeCalled()
})
expect(onContinue.mock.calls[0][0]).toEqual(sheetNames[selectSheetIndex])
})
test("Should show error toast if error is thrown in uploadStepHook", async () => {
const uploadStepHook = jest.fn(async () => {
throw new Error(ERROR_MESSAGE)
return undefined as any
})
render(<ReactSpreadsheetImport {...mockRsiValues} uploadStepHook={uploadStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
const data = readFileSync(__dirname + "/../../../../static/Workbook1.xlsx")
fireEvent.drop(uploader, {
target: {
files: [
new File([data], "testFile.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
},
})
const nextButton = await screen.findByRole(
"button",
{
name: "Next",
},
{ timeout: 5000 },
)
await userEvent.click(nextButton)
const errorToast = await screen.findAllByText(ERROR_MESSAGE, undefined, { timeout: 5000 })
expect(errorToast?.[0]).toBeInTheDocument()
})

View File

@@ -1,4 +1,4 @@
import type XLSX from "xlsx-ugnis" import type XLSX from "xlsx"
import { useCallback, useState } from "react" import { useCallback, useState } from "react"
import { useRsi } from "../../hooks/useRsi" import { useRsi } from "../../hooks/useRsi"
import { DropZone } from "./components/DropZone" import { DropZone } from "./components/DropZone"

View File

@@ -1,22 +0,0 @@
import { UploadStep } from "../UploadStep"
import { defaultTheme } from "../../../ReactSpreadsheetImport"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
export default {
title: "Upload Step",
parameters: {
layout: "fullscreen",
},
}
export const Basic = () => {
return (
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<UploadStep onContinue={async () => {}} />
</ModalWrapper>
</Providers>
)
}

View File

@@ -1,86 +0,0 @@
import "@testing-library/jest-dom"
import { render, fireEvent, waitFor, screen } from "@testing-library/react"
import { UploadStep } from "../UploadStep"
import { defaultTheme, ReactSpreadsheetImport } from "../../../ReactSpreadsheetImport"
import { mockRsiValues } from "../../../stories/mockRsiValues"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
const MUTATED_RAW_DATA = "Bye"
const ERROR_MESSAGE = "Something happened while uploading"
test("Upload a file", async () => {
const file = new File(["Hello, Hello, Hello, Hello"], "test.csv", { type: "text/csv" })
const onContinue = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<UploadStep onContinue={onContinue} />
</ModalWrapper>
</Providers>,
)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
await waitFor(
() => {
expect(onContinue).toBeCalled()
},
{ timeout: 5000 },
)
})
test("Should call uploadStepHook on file upload", async () => {
const file = new File(["Hello, Hello, Hello, Hello"], "test.csv", { type: "text/csv" })
const uploadStepHook = jest.fn(async (values) => {
return values
})
render(<ReactSpreadsheetImport {...mockRsiValues} uploadStepHook={uploadStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
await waitFor(
() => {
expect(uploadStepHook).toBeCalled()
},
{ timeout: 5000 },
)
})
test("uploadStepHook should be able to mutate raw upload data", async () => {
const file = new File(["Hello, Hello, Hello, Hello"], "test.csv", { type: "text/csv" })
const uploadStepHook = jest.fn(async ([[, ...values]]) => {
return [[MUTATED_RAW_DATA, ...values]]
})
render(<ReactSpreadsheetImport {...mockRsiValues} uploadStepHook={uploadStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
const el = await screen.findByText(MUTATED_RAW_DATA, undefined, { timeout: 5000 })
expect(el).toBeInTheDocument()
})
test("Should show error toast if error is thrown in uploadStepHook", async () => {
const file = new File(["Hello, Hello, Hello, Hello"], "test.csv", { type: "text/csv" })
const uploadStepHook = jest.fn(async () => {
throw new Error(ERROR_MESSAGE)
return undefined as any
})
render(<ReactSpreadsheetImport {...mockRsiValues} uploadStepHook={uploadStepHook} />)
const uploader = screen.getByTestId("rsi-dropzone")
fireEvent.drop(uploader, {
target: { files: [file] },
})
const errorToast = await screen.findAllByText(ERROR_MESSAGE, undefined, { timeout: 5000 })
expect(errorToast?.[0]).toBeInTheDocument()
})

View File

@@ -1,10 +1,17 @@
import DataGrid, { Column, useRowSelection } from "react-data-grid" import type { Column as RDGColumn, RenderEditCellProps, FormatterProps } from "react-data-grid"
import { Box, Checkbox, Input, Switch, Tooltip } from "@chakra-ui/react" import { useRowSelection } from "react-data-grid"
import type { Data, Fields } from "../../../types" import { Checkbox, Input, Switch } from "@chakra-ui/react"
import type { Data, Fields, Field, SelectOption } from "../../../types"
import type { ChangeEvent } from "react" import type { ChangeEvent } from "react"
import type { Meta } from "../types" import type { Meta } from "../types"
import { CgInfo } from "react-icons/cg" import { CgInfo } from "react-icons/cg"
import { TableSelect } from "../../../components/Selects/TableSelect" import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const SELECT_COLUMN_KEY = "select-row" const SELECT_COLUMN_KEY = "select-row"
@@ -13,7 +20,9 @@ function autoFocusAndSelect(input: HTMLInputElement | null) {
input?.select() input?.select()
} }
export const generateColumns = <T extends string>(fields: Fields<T>): Column<Data<T> & Meta>[] => [ type RowType<T extends string> = Data<T> & Meta
export const generateColumns = <T extends string>(fields: Fields<T>): RDGColumn<RowType<T>>[] => [
{ {
key: SELECT_COLUMN_KEY, key: SELECT_COLUMN_KEY,
name: "", name: "",
@@ -24,7 +33,7 @@ export const generateColumns = <T extends string>(fields: Fields<T>): Column<Dat
sortable: false, sortable: false,
frozen: true, frozen: true,
cellClass: "rdg-checkbox", cellClass: "rdg-checkbox",
formatter: (props) => { formatter: (props: FormatterProps<RowType<T>>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection() const [isRowSelected, onRowSelectionChange] = useRowSelection()
return ( return (
@@ -44,44 +53,58 @@ export const generateColumns = <T extends string>(fields: Fields<T>): Column<Dat
}, },
}, },
...fields.map( ...fields.map(
(column): Column<Data<T> & Meta> => ({ (column: Field<T>): RDGColumn<RowType<T>> => ({
key: column.key, key: column.key,
name: column.label, name: column.label,
minWidth: 150, minWidth: 150,
resizable: true, resizable: true,
headerRenderer: () => ( headerRenderer: () => (
<Box display="flex" gap={1} alignItems="center" position="relative"> <div className="flex gap-1 items-center relative">
<Box flex={1} overflow="hidden" textOverflow="ellipsis"> <div className="flex-1 overflow-hidden text-ellipsis">
{column.label} {column.label}
</Box> </div>
{column.description && ( {column.description && (
<Tooltip placement="top" hasArrow label={column.description}> <div className="flex-none">
<Box flex={"0 0 auto"}> <CgInfo className="h-4 w-4" />
<CgInfo size="16px" /> </div>
</Box>
</Tooltip>
)} )}
</Box> </div>
), ),
editable: column.fieldType.type !== "checkbox", editable: column.fieldType.type !== "checkbox",
editor: ({ row, onRowChange, onClose }) => { editor: ({ row, onRowChange, onClose }: RenderEditCellProps<RowType<T>>) => {
let component let component
switch (column.fieldType.type) { switch (column.fieldType.type) {
case "select": case "select":
component = ( component = (
<TableSelect <Select
value={column.fieldType.options.find((option) => option.value === (row[column.key] as string))} defaultOpen
onChange={(value) => { value={row[column.key] as string}
onRowChange({ ...row, [column.key]: value?.value }, true) onValueChange={(value) => {
onRowChange({ ...row, [column.key]: value }, true)
}} }}
options={column.fieldType.options} >
/> <SelectTrigger className="w-full border-0 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent
position="popper"
className="z-[1000]"
align="start"
side="bottom"
>
{column.fieldType.options.map((option: SelectOption) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) )
break break
default: default:
component = ( component = (
<Box paddingInlineStart="0.5rem"> <div className="pl-2">
<Input <Input
ref={autoFocusAndSelect} ref={autoFocusAndSelect}
variant="unstyled" variant="unstyled"
@@ -93,7 +116,7 @@ export const generateColumns = <T extends string>(fields: Fields<T>): Column<Dat
}} }}
onBlur={() => onClose(true)} onBlur={() => onClose(true)}
/> />
</Box> </div>
) )
} }
@@ -102,16 +125,14 @@ export const generateColumns = <T extends string>(fields: Fields<T>): Column<Dat
editorOptions: { editorOptions: {
editOnClick: true, editOnClick: true,
}, },
formatter: ({ row, onRowChange }) => { formatter: ({ row, onRowChange }: FormatterProps<RowType<T>>) => {
let component let component
switch (column.fieldType.type) { switch (column.fieldType.type) {
case "checkbox": case "checkbox":
component = ( component = (
<Box <div
display="flex" className="flex items-center h-full"
alignItems="center"
height="100%"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
}} }}
@@ -122,29 +143,32 @@ export const generateColumns = <T extends string>(fields: Fields<T>): Column<Dat
onRowChange({ ...row, [column.key]: !row[column.key as T] }) onRowChange({ ...row, [column.key]: !row[column.key as T] })
}} }}
/> />
</Box> </div>
) )
break break
case "select": case "select":
component = ( component = (
<Box minWidth="100%" minHeight="100%" overflow="hidden" textOverflow="ellipsis"> <div className="min-w-full min-h-full overflow-hidden text-ellipsis">
{column.fieldType.options.find((option) => option.value === row[column.key as T])?.label || null} {column.fieldType.options.find((option: SelectOption) => option.value === row[column.key as T])?.label || null}
</Box> </div>
) )
break break
default: default:
component = ( component = (
<Box minWidth="100%" minHeight="100%" overflow="hidden" textOverflow="ellipsis"> <div className="min-w-full min-h-full overflow-hidden text-ellipsis">
{row[column.key as T]} {row[column.key as T]}
</Box> </div>
) )
} }
if (row.__errors?.[column.key]) { if (row.__errors?.[column.key]) {
return ( return (
<Tooltip placement="top" hasArrow label={row.__errors?.[column.key]?.message}> <div className="relative group">
{component} {component}
</Tooltip> <div className="absolute left-0 -top-8 z-50 hidden group-hover:block bg-popover text-popover-foreground text-sm p-2 rounded shadow">
{row.__errors?.[column.key]?.message}
</div>
</div>
) )
} }

View File

@@ -1,26 +0,0 @@
import { editableTableInitialData, mockRsiValues } from "../../../stories/mockRsiValues"
import { ValidationStep } from "../ValidationStep"
import { Providers } from "../../../components/Providers"
import { defaultTheme } from "../../../ReactSpreadsheetImport"
import { ModalWrapper } from "../../../components/ModalWrapper"
import { addErrorsAndRunHooks } from "../utils/dataMutations"
export default {
title: "Validation Step",
parameters: {
layout: "fullscreen",
},
}
const file = new File([""], "file.csv")
const data = await addErrorsAndRunHooks(editableTableInitialData, mockRsiValues.fields)
export const Basic = () => {
return (
<Providers theme={defaultTheme} rsiValues={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep initialData={data} file={file} />
</ModalWrapper>
</Providers>
)
}

View File

@@ -1,956 +0,0 @@
import "@testing-library/jest-dom"
import { render, waitFor, screen, act } from "@testing-library/react"
import { ValidationStep } from "../ValidationStep"
import { defaultRSIProps, defaultTheme } from "../../../ReactSpreadsheetImport"
import { Providers } from "../../../components/Providers"
import { ModalWrapper } from "../../../components/ModalWrapper"
import userEvent from "@testing-library/user-event"
import { translations } from "../../../translationsRSIProps"
import { addErrorsAndRunHooks } from "../utils/dataMutations"
import { Fields, RowHook, TableHook } from "../../../types"
type fieldKeys<T extends Fields<string>> = T[number]["key"]
const mockValues = {
...defaultRSIProps,
fields: [],
onSubmit: () => {},
isOpen: true,
onClose: () => {},
} as const
const getFilterSwitch = () =>
screen.getByRole("checkbox", {
name: translations.validationStep.filterSwitchTitle,
})
const file = new File([""], "file.csv")
describe("Validation step tests", () => {
test("Submit data", async () => {
const onSubmit = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, onSubmit: onSubmit }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep initialData={[]} file={file} />
</ModalWrapper>
</Providers>,
)
const finishButton = screen.getByRole("button", {
name: "Confirm",
})
await userEvent.click(finishButton)
await waitFor(() => {
expect(onSubmit).toBeCalledWith({ all: [], invalidData: [], validData: [] }, file)
})
})
test("Submit data without returning promise", async () => {
const onSuccess = jest.fn()
const onSubmit = jest.fn(() => {
onSuccess()
})
const onClose = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, onSubmit, onClose }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep initialData={[]} file={file} />
</ModalWrapper>
</Providers>,
)
const finishButton = screen.getByRole("button", {
name: "Confirm",
})
await userEvent.click(finishButton)
await waitFor(() => {
expect(onSubmit).toBeCalledWith({ all: [], invalidData: [], validData: [] }, file)
})
await waitFor(() => {
expect(onSuccess).toBeCalled()
expect(onClose).toBeCalled()
})
})
test("Submit data with a successful async return", async () => {
const onSuccess = jest.fn()
const onSubmit = jest.fn(async (): Promise<void> => {
onSuccess()
return Promise.resolve()
})
const onClose = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, onSubmit, onClose }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep initialData={[]} file={file} />
</ModalWrapper>
</Providers>,
)
const finishButton = screen.getByRole("button", {
name: "Confirm",
})
await userEvent.click(finishButton)
await waitFor(() => {
expect(onSubmit).toBeCalledWith({ all: [], invalidData: [], validData: [] }, file)
})
await waitFor(() => {
expect(onSuccess).toBeCalled()
expect(onClose).toBeCalled()
})
})
test("Submit data with a unsuccessful async return", async () => {
const ERROR_MESSAGE = "ERROR has occurred"
const onReject = jest.fn()
const onSubmit = jest.fn(async (): Promise<void> => {
onReject()
throw new Error(ERROR_MESSAGE)
})
const onClose = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, onSubmit, onClose }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep initialData={[]} file={file} />
</ModalWrapper>
</Providers>,
)
const finishButton = screen.getByRole("button", {
name: "Confirm",
})
await userEvent.click(finishButton)
await waitFor(() => {
expect(onSubmit).toBeCalledWith({ all: [], invalidData: [], validData: [] }, file)
})
const errorToast = await screen.findAllByText(ERROR_MESSAGE, undefined, { timeout: 5000 })
expect(onReject).toBeCalled()
expect(errorToast?.[0]).toBeInTheDocument()
expect(onClose).not.toBeCalled()
})
test("Filters rows with required errors", async () => {
const UNIQUE_NAME = "very unique name"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
validations: [
{
rule: "required",
errorMessage: "Name is required",
},
],
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: UNIQUE_NAME,
},
{
name: undefined,
},
],
fields,
)
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(3)
const validRow = screen.getByText(UNIQUE_NAME)
expect(validRow).toBeInTheDocument()
const switchFilter = getFilterSwitch()
await userEvent.click(switchFilter)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(2)
})
test("Filters rows with errors, fixes row, removes filter", async () => {
const UNIQUE_NAME = "very unique name"
const SECOND_UNIQUE_NAME = "another unique name"
const FINAL_NAME = "just name"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
validations: [
{
rule: "required",
errorMessage: "Name is required",
},
],
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: UNIQUE_NAME,
},
{
name: undefined,
},
{
name: SECOND_UNIQUE_NAME,
},
],
fields,
)
const onSubmit = jest.fn()
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields, onSubmit }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(4)
const validRow = screen.getByText(UNIQUE_NAME)
expect(validRow).toBeInTheDocument()
const switchFilter = getFilterSwitch()
await userEvent.click(switchFilter)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(2)
// don't really know another way to select an empty cell
const emptyCell = screen.getAllByRole("gridcell", { name: undefined })[1]
await userEvent.click(emptyCell)
await userEvent.keyboard(FINAL_NAME + "{enter}")
const filteredRowsNoErrorsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsNoErrorsWithHeader).toHaveLength(1)
await userEvent.click(switchFilter)
const allRowsFixedWithHeader = await screen.findAllByRole("row")
expect(allRowsFixedWithHeader).toHaveLength(4)
const finishButton = screen.getByRole("button", {
name: "Confirm",
})
await userEvent.click(finishButton)
await waitFor(() => {
expect(onSubmit).toBeCalled()
})
})
test("Filters rows with unique errors", async () => {
const NON_UNIQUE_NAME = "very unique name"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
validations: [
{
rule: "unique",
errorMessage: "Name must be unique",
},
],
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: NON_UNIQUE_NAME,
},
{
name: NON_UNIQUE_NAME,
},
{
name: "I am fine",
},
],
fields,
)
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(4)
const switchFilter = getFilterSwitch()
await userEvent.click(switchFilter)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(3)
})
test("Filters rows with regex errors", async () => {
const NOT_A_NUMBER = "not a number"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
validations: [
{
rule: "regex",
errorMessage: "Name must be unique",
value: "^[0-9]*$",
},
],
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: NOT_A_NUMBER,
},
{
name: "1234",
},
{
name: "9999999",
},
],
fields,
)
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(4)
const switchFilter = getFilterSwitch()
await userEvent.click(switchFilter)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(2)
})
test("Deletes selected rows", async () => {
const FIRST_DELETE = "first"
const SECOND_DELETE = "second"
const THIRD = "third"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: FIRST_DELETE,
},
{
name: SECOND_DELETE,
},
{
name: THIRD,
},
],
fields,
)
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(4)
const switchFilters = screen.getAllByRole("checkbox", {
name: "Select",
})
await userEvent.click(switchFilters[0])
await userEvent.click(switchFilters[1])
const discardButton = screen.getByRole("button", {
name: "Discard selected rows",
})
await userEvent.click(discardButton)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(2)
const validRow = screen.getByText(THIRD)
expect(validRow).toBeInTheDocument()
})
test("Deletes selected rows, changes the last one", async () => {
const FIRST_DELETE = "first"
const SECOND_DELETE = "second"
const THIRD = "third"
const THIRD_CHANGED = "third_changed"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: FIRST_DELETE,
},
{
name: SECOND_DELETE,
},
{
name: THIRD,
},
],
fields,
)
render(
<Providers theme={defaultTheme} rsiValues={{ ...mockValues, fields }}>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const allRowsWithHeader = await screen.findAllByRole("row")
expect(allRowsWithHeader).toHaveLength(4)
const switchFilters = screen.getAllByRole("checkbox", {
name: "Select",
})
await userEvent.click(switchFilters[0])
await userEvent.click(switchFilters[1])
const discardButton = screen.getByRole("button", {
name: "Discard selected rows",
})
await userEvent.click(discardButton)
const filteredRowsWithHeader = await screen.findAllByRole("row")
expect(filteredRowsWithHeader).toHaveLength(2)
const nameCell = screen.getByRole("gridcell", {
name: THIRD,
})
await userEvent.click(nameCell)
screen.getByRole<HTMLInputElement>("textbox")
await userEvent.keyboard(THIRD_CHANGED + "{enter}")
const validRow = screen.getByText(THIRD_CHANGED)
expect(validRow).toBeInTheDocument()
})
test("All inputs change values", async () => {
const NAME = "John"
const NEW_NAME = "Johnny"
const OPTIONS = [
{ value: "one", label: "ONE" },
{ value: "two", label: "TWO" },
] as const
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
{
label: "lastName",
key: "lastName",
fieldType: {
type: "select",
options: OPTIONS,
},
},
{
label: "is cool",
key: "is_cool",
fieldType: {
type: "checkbox",
},
},
] as const
const initialData = await addErrorsAndRunHooks(
[
{
name: NAME,
lastName: OPTIONS[0].value,
is_cool: false,
},
],
fields,
)
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
// input
const nameCell = screen.getByRole("gridcell", {
name: NAME,
})
await userEvent.click(nameCell)
const input: HTMLInputElement | null = screen.getByRole<HTMLInputElement>("textbox")
expect(input).toHaveValue(NAME)
expect(input).toHaveFocus()
expect(input.selectionStart).toBe(0)
expect(input.selectionEnd).toBe(NAME.length)
await userEvent.keyboard(NEW_NAME + "{enter}")
expect(input).not.toBeInTheDocument()
const newNameCell = screen.getByRole("gridcell", {
name: NEW_NAME,
})
expect(newNameCell).toBeInTheDocument()
// select
const lastNameCell = screen.getByRole("gridcell", {
name: OPTIONS[0].label,
})
await userEvent.click(lastNameCell)
const newOption = screen.getByRole("button", {
name: OPTIONS[1].label,
})
await userEvent.click(newOption)
expect(newOption).not.toBeInTheDocument()
const newLastName = screen.getByRole("gridcell", {
name: OPTIONS[1].label,
})
expect(newLastName).toBeInTheDocument()
// Boolean
const checkbox = screen.getByRole("checkbox", {
name: "",
})
expect(checkbox).not.toBeChecked()
await userEvent.click(checkbox)
expect(checkbox).toBeChecked()
})
test("Row hook transforms data", async () => {
const NAME = "John"
const LASTNAME = "Doe"
const NEW_NAME = "Johnny"
const NEW_LASTNAME = "CENA"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
{
label: "lastName",
key: "lastName",
fieldType: {
type: "input",
},
},
] as const
const rowHook: RowHook<fieldKeys<typeof fields>> = (value) => ({
name: value.name?.toString()?.split(/(\s+)/)[0],
lastName: value.name?.toString()?.split(/(\s+)/)[2],
})
const initialData = await addErrorsAndRunHooks(
[
{
name: NAME + " " + LASTNAME,
lastName: undefined,
},
],
fields,
rowHook,
)
await act(async () => {
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
rowHook,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
})
const nameCell = screen.getByRole("gridcell", {
name: NAME,
})
expect(nameCell).toBeInTheDocument()
const lastNameCell = screen.getByRole("gridcell", {
name: LASTNAME,
})
expect(lastNameCell).toBeInTheDocument()
// activate input
await userEvent.click(nameCell)
await userEvent.keyboard(NEW_NAME + " " + NEW_LASTNAME + "{enter}")
const newNameCell = screen.getByRole("gridcell", {
name: NEW_NAME,
})
expect(newNameCell).toBeInTheDocument()
const newLastNameCell = screen.getByRole("gridcell", {
name: NEW_LASTNAME,
})
expect(newLastNameCell).toBeInTheDocument()
})
test("Row hook only runs on a single row", async () => {
const NAME = "John"
const NEW_NAME = "Kate"
const LAST_NAME = "Doe"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
{
label: "lastName",
key: "lastName",
fieldType: {
type: "input",
},
},
] as const
const mockedHook = jest.fn((a) => a)
const initialData = await addErrorsAndRunHooks(
[
{
name: NAME,
lastName: LAST_NAME,
},
{
name: "Johnny",
lastName: "Doeson",
},
],
fields,
mockedHook,
)
await act(async () => {
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
rowHook: mockedHook,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
})
// initially row hook is called for each row
expect(mockedHook.mock.calls.length).toBe(2)
const nameCell = screen.getByRole("gridcell", {
name: NAME,
})
expect(nameCell).toBeInTheDocument()
// activate input
await userEvent.click(nameCell)
await userEvent.keyboard(NEW_NAME + "{enter}")
expect(mockedHook.mock.calls[2][0]?.name).toBe(NEW_NAME)
expect(mockedHook.mock.calls.length).toBe(3)
})
test("Row hook raises error", async () => {
const WRONG_NAME = "Johnny"
const RIGHT_NAME = "Jonathan"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
] as const
const rowHook: RowHook<fieldKeys<typeof fields>> = (value, setError) => {
if (value.name === WRONG_NAME) {
setError(fields[0].key, { message: "Wrong name", level: "error" })
}
return value
}
const initialData = await addErrorsAndRunHooks(
[
{
name: WRONG_NAME,
},
],
fields,
rowHook,
)
await act(async () =>
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
rowHook,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
),
)
const switchFilter = getFilterSwitch()
await expect(await screen.findAllByRole("row")).toHaveLength(2)
await userEvent.click(switchFilter)
await expect(await screen.findAllByRole("row")).toHaveLength(2)
const nameCell = screen.getByRole("gridcell", {
name: WRONG_NAME,
})
expect(nameCell).toBeInTheDocument()
await userEvent.click(nameCell)
screen.getByRole<HTMLInputElement>("textbox")
await userEvent.keyboard(RIGHT_NAME + "{enter}")
await expect(await screen.findAllByRole("row")).toHaveLength(1)
})
test("Table hook transforms data", async () => {
const NAME = "John"
const SECOND_NAME = "Doe"
const NEW_NAME = "Jakee"
const ADDITION = "last"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
] as const
const tableHook: TableHook<fieldKeys<typeof fields>> = (data) =>
data.map((value) => ({
name: value.name + ADDITION,
}))
const initialData = await addErrorsAndRunHooks(
[
{
name: NAME,
},
{
name: SECOND_NAME,
},
],
fields,
undefined,
tableHook,
)
await act(async () => {
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
tableHook,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
})
const nameCell = screen.getByRole("gridcell", {
name: NAME + ADDITION,
})
expect(nameCell).toBeInTheDocument()
const lastNameCell = screen.getByRole("gridcell", {
name: SECOND_NAME + ADDITION,
})
expect(lastNameCell).toBeInTheDocument()
// activate input
await userEvent.click(nameCell)
await userEvent.keyboard(NEW_NAME + "{enter}")
const newNameCell = screen.getByRole("gridcell", {
name: NEW_NAME + ADDITION,
})
expect(newNameCell).toBeInTheDocument()
})
test("Table hook raises error", async () => {
const WRONG_NAME = "Johnny"
const RIGHT_NAME = "Jonathan"
const fields = [
{
label: "Name",
key: "name",
fieldType: {
type: "input",
},
},
] as const
const tableHook: TableHook<fieldKeys<typeof fields>> = (data, setError) => {
data.forEach((value, index) => {
if (value.name === WRONG_NAME) {
setError(index, fields[0].key, { message: "Wrong name", level: "error" })
}
return value
})
return data
}
const initialData = await addErrorsAndRunHooks(
[
{
name: WRONG_NAME,
},
{
name: WRONG_NAME,
},
],
fields,
undefined,
tableHook,
)
render(
<Providers
theme={defaultTheme}
rsiValues={{
...mockValues,
fields,
tableHook,
}}
>
<ModalWrapper isOpen={true} onClose={() => {}}>
<ValidationStep<fieldKeys<typeof fields>> initialData={initialData} file={file} />
</ModalWrapper>
</Providers>,
)
const switchFilter = getFilterSwitch()
await expect(await screen.findAllByRole("row")).toHaveLength(3)
await userEvent.click(switchFilter)
await expect(await screen.findAllByRole("row")).toHaveLength(3)
const nameCell = await screen.getAllByRole("gridcell", {
name: WRONG_NAME,
})[0]
await userEvent.click(nameCell)
screen.getByRole<HTMLInputElement>("textbox")
await userEvent.keyboard(RIGHT_NAME + "{enter}")
await expect(await screen.findAllByRole("row")).toHaveLength(2)
})
})

View File

@@ -1,58 +0,0 @@
import { ReactSpreadsheetImport } from "../ReactSpreadsheetImport"
import { Box, Link, Code, Button, useDisclosure } from "@chakra-ui/react"
import { mockRsiValues } from "./mockRsiValues"
import { useState } from "react"
import type { Result } from "src/types"
export default {
title: "React spreadsheet import",
}
export const Basic = () => {
const [data, setData] = useState<Result<any> | null>(null)
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<>
<Box py={20} display="flex" gap="8px" alignItems="center">
<Button onClick={onOpen} border="2px solid #7069FA" p="8px" borderRadius="8px">
Open Flow
</Button>
(make sure you have a file to upload)
</Box>
<Link href="./exampleFile.csv" border="2px solid #718096" p="8px" borderRadius="8px" download="exampleCSV">
Download example file
</Link>
<ReactSpreadsheetImport {...mockRsiValues} isOpen={isOpen} onClose={onClose} onSubmit={setData} />
{!!data && (
<Box pt={64} display="flex" gap="8px" flexDirection="column">
<b>Returned data (showing first 100 rows):</b>
<Code
display="flex"
alignItems="center"
borderRadius="16px"
fontSize="12px"
background="#4A5568"
color="white"
p={32}
>
<pre>
{JSON.stringify(
{
validData: data.validData.slice(0, 100),
invalidData: data.invalidData.slice(0, 100),
all: data.all.slice(0, 100),
},
undefined,
4,
)}
</pre>
</Code>
</Box>
)}
</>
)
}
Basic.parameters = {
chromatic: { disableSnapshot: true },
}

View File

@@ -1,162 +0,0 @@
import type { RsiProps } from "../types"
import { defaultRSIProps } from "../ReactSpreadsheetImport"
const fields = [
{
label: "Name",
key: "name",
alternateMatches: ["first name", "first"],
fieldType: {
type: "input",
},
example: "Stephanie",
validations: [
{
rule: "required",
errorMessage: "Name is required",
},
],
},
{
label: "Surname",
key: "surname",
alternateMatches: ["second name", "last name", "last"],
fieldType: {
type: "input",
},
example: "McDonald",
validations: [
{
rule: "unique",
errorMessage: "Last name must be unique",
level: "info",
},
],
description: "Family / Last name",
},
{
label: "Age",
key: "age",
alternateMatches: ["years"],
fieldType: {
type: "input",
},
example: "23",
validations: [
{
rule: "regex",
value: "^\\d+$",
errorMessage: "Age must be a number",
level: "warning",
},
],
},
{
label: "Team",
key: "team",
alternateMatches: ["department"],
fieldType: {
type: "select",
options: [
{ label: "Team One", value: "one" },
{ label: "Team Two", value: "two" },
],
},
example: "Team one",
validations: [
{
rule: "required",
errorMessage: "Team is required",
},
],
},
{
label: "Is manager",
key: "is_manager",
alternateMatches: ["manages"],
fieldType: {
type: "checkbox",
booleanMatches: {},
},
example: "true",
},
] as const
const mockComponentBehaviourForTypes = <T extends string>(props: RsiProps<T>) => props
export const mockRsiValues = mockComponentBehaviourForTypes({
...defaultRSIProps,
fields: fields,
onSubmit: (data) => {
console.log(data.all.map((value) => value))
},
isOpen: true,
onClose: () => {},
// uploadStepHook: async (data) => {
// await new Promise((resolve) => {
// setTimeout(() => resolve(data), 4000)
// })
// return data
// },
// selectHeaderStepHook: async (hData, data) => {
// await new Promise((resolve) => {
// setTimeout(
// () =>
// resolve({
// headerValues: hData,
// data,
// }),
// 4000,
// )
// })
// return {
// headerValues: hData,
// data,
// }
// },
// // Runs after column matching and on entry change, more performant
// matchColumnsStepHook: async (data) => {
// await new Promise((resolve) => {
// setTimeout(() => resolve(data), 4000)
// })
// return data
// },
})
export const editableTableInitialData = [
{
name: "Hello",
surname: "Hello",
age: "123123",
team: "one",
is_manager: true,
},
{
name: "Hello",
surname: "Hello",
age: "12312zsas3",
team: "two",
is_manager: true,
},
{
name: "Whooaasdasdawdawdawdiouasdiuasdisdhasd",
surname: "Hello",
age: "123123",
team: undefined,
is_manager: false,
},
{
name: "Goodbye",
surname: "Goodbye",
age: "111",
team: "two",
is_manager: true,
},
]
export const headerSelectionTableFields = [
["text", "num", "select", "bool"],
["second", "123", "one", "true"],
["third", "123", "one", "true"],
["fourth", "123", "one", "true"],
]

View File

@@ -1,24 +0,0 @@
import "@testing-library/jest-dom"
import { render } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { ReactSpreadsheetImport } from "../ReactSpreadsheetImport"
import { mockRsiValues } from "../stories/mockRsiValues"
test("Close modal", async () => {
let isOpen = true
const onClose = jest.fn(() => {
isOpen = !isOpen
})
const { getByText, getByLabelText } = render(
<ReactSpreadsheetImport {...mockRsiValues} onClose={onClose} isOpen={isOpen} />,
)
const closeButton = getByLabelText("Close modal")
await userEvent.click(closeButton)
const confirmButton = getByText("Exit flow")
await userEvent.click(confirmButton)
expect(onClose).toBeCalled()
})

View File

@@ -1,63 +0,0 @@
// Yeeted from https://github.com/adazzle/react-data-grid/blob/main/test/setup.ts
if (typeof window !== "undefined") {
window.ResizeObserver ??= class {
callback: ResizeObserverCallback
constructor(callback: ResizeObserverCallback) {
this.callback = callback
}
observe() {
this.callback([], this)
}
unobserve() {}
disconnect() {}
}
// patch clientWidth/clientHeight to pretend we're rendering DataGrid at 1080p
Object.defineProperties(HTMLDivElement.prototype, {
clientWidth: {
get(this: HTMLDivElement) {
return this.classList.contains("rdg") ? 1920 : 0
},
},
clientHeight: {
get(this: HTMLDivElement) {
return this.classList.contains("rdg") ? 1080 : 0
},
},
})
Element.prototype.setPointerCapture ??= () => {}
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
Object.defineProperty(global, "ResizeObserver", {
writable: true,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
})),
})
Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
writable: true,
value: jest.fn(),
})
}
jest.setTimeout(30000)