Skip to content

Commit

Permalink
Merge pull request #16 from UgnisSoftware/UGN-213
Browse files Browse the repository at this point in the history
feature UGN-213 - add validations
  • Loading branch information
masiulis authored Feb 18, 2022
2 parents 184d277 + 991a646 commit 2fb66f8
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/components/DropZone.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, Text, useTheme, useToast } from "@chakra-ui/react"
import { useDropzone } from "react-dropzone"
import XLSX from "xlsx"
import * as XLSX from "xlsx"
import { useState } from "react"
import { getDropZoneBorder } from "../utils/getDropZoneBorder"

Expand Down
16 changes: 9 additions & 7 deletions src/components/EditableTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ const createGlobalStyleOverride = () => css`
--rdg-selection-color: none;
}
.rdg-header-row .rdg-cell {
--rdg-selection-color: none;
border: none;
}
.rdg-checkbox {
--rdg-selection-color: none;
background-color: var(--rdg-header-background-color);
Expand All @@ -51,14 +46,21 @@ const createGlobalStyleOverride = () => css`
}
.rdg-cell[aria-selected="true"] {
border-radius: 2px;
box-shadow: inset 0 0 0 1px var(--rdg-selection-color);
}
.rdg-cell-error {
background-color: var(--chakra-colors-red-50);
box-shadow: inset 0 0 0 1px var(--chakra-colors-red-100);
}
.rdg-cell-warning {
background-color: var(--chakra-colors-orange-50);
box-shadow: 0 1px 0 0 var(--chakra-colors-orange-100);
}
.rdg-cell-info {
background-color: var(--chakra-colors-blue-50);
box-shadow: inset 0 0 0 1px var(--chakra-colors-blue-100);
}
.rdg {
contain: size layout style paint;
Expand All @@ -74,7 +76,7 @@ const createGlobalStyleOverride = () => css`
--rdg-font-size: 14px;
}
`
const ROW_HEIGHT = 42
const ROW_HEIGHT = 35

interface Props<Data> extends DataGridProps<Data> {
rowHeight?: number
Expand Down
162 changes: 133 additions & 29 deletions src/stories/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Box, ChakraProvider, Checkbox, extendTheme, Input, Select, Switch } from "@chakra-ui/react"
import { EditableTable } from "../components/EditableTable"
import { connect, useLape } from "lape"
import { connect, ignoreState, useLape } from "lape"
import type { ChangeEvent } from "react"
import { colorSchemeOverrides, themeOverrides } from "../theme"
import type { Field, Fields } from "../types"
import type { Field, Fields, Info } from "../types"
import type { Column, FormatterProps } from "react-data-grid"
import { useRowSelection } from "react-data-grid"
export default {
title: "React spreadsheet import",
}

export const SELECT_COLUMN_KEY = "select-row"
type Errors = { [id: string]: { [key: string]: Info } }
type Data = { [key: string]: string | number | boolean | null }[]

const SELECT_COLUMN_KEY = "select-row"

function SelectFormatter(props: FormatterProps<unknown>) {
const [isRowSelected, onRowSelectionChange] = useRowSelection()
Expand Down Expand Up @@ -48,26 +51,22 @@ const theme = extendTheme(colorSchemeOverrides, themeOverrides)
const TableComponent = connect(() => {
const data = [
{
id: 0,
test: "Hello",
second: "one",
bool: true,
},
{
id: 1,
test: "Hello",
test: "123123",
second: "two",
bool: true,
},
{
id: 2,
test: "Hello",
second: "one",
test: "123123",
second: null,
bool: false,
},
{
id: 3,
test: "Hello",
test: "111",
second: "two",
bool: true,
},
Expand All @@ -78,6 +77,19 @@ const TableComponent = connect(() => {
key: "test",
label: "Tests",
fieldType: { type: "input" },
validations: [
{
rule: "unique",
errorMessage: "Test must be unique",
level: "info",
},
{
rule: "regex",
value: "^\\d+$",
errorMessage: "Test must be number",
level: "warning",
},
],
},
{
key: "second",
Expand All @@ -89,6 +101,12 @@ const TableComponent = connect(() => {
{ label: "Two", value: "two" },
],
},
validations: [
{
rule: "required",
errorMessage: "Second is required",
},
],
},
{
key: "bool",
Expand All @@ -97,41 +115,100 @@ const TableComponent = connect(() => {
},
]

const runValidation = (data: Data): Errors => {
let errors: Errors = {}
fields.forEach((field) => {
field.validations?.forEach((validation) => {
switch (validation.rule) {
case "unique": {
const values = data.map((entry) => entry[field.key])
values.forEach((value, index) => {
if (values.indexOf(value) !== values.lastIndexOf(value)) {
errors[index] = {
...errors[index],
[field.key]: {
level: validation.level || "error",
message: validation.errorMessage || "Field must be unique",
},
}
}
})
break
}
case "required": {
data.forEach((entry, index) => {
if (entry[field.key] === null || entry[field.key] === undefined || entry[field.key] === "") {
errors[index] = {
...errors[index],
[field.key]: {
level: validation.level || "error",
message: validation.errorMessage || "Field is required",
},
}
}
})
break
}
case "regex": {
const regex = new RegExp(validation.value, validation.flags)
data.forEach((entry, index) => {
if (!entry[field.key]?.toString()?.match(regex)) {
errors[index] = {
...errors[index],
[field.key]: {
level: validation.level || "error",
message:
validation.errorMessage ||
`Field did not match the regex /${validation.value}/${validation.flags} `,
},
}
}
})
break
}
}
})
})
return errors
}

const state = useLape<{
data: any[]
data: Data
errorCount: number
filterErrors: boolean
errors: Errors
selectedRows: ReadonlySet<number | string>
}>({
data: data,
data: ignoreState(data),
errorCount: 0,
errors: runValidation(data),
filterErrors: false,
selectedRows: new Set(),
})

const updateSelect = (row: any, key: string) => (event: ChangeEvent<HTMLSelectElement>) => {
row[key] = event.target.value
}
const updateInput = (row: any, key: string) => (event: ChangeEvent<HTMLInputElement>) => {
row[key] = event.target.value
const updateRow = (rows: any[]) => {
state.data = rows
state.errors = runValidation(state.data)
}
const updateSwitch = () => {}
const columns = [
SelectColumn,
...fields.map((column: Field) => ({
key: column.key,
name: column.label,
resizable: true,
editable: column.fieldType.type !== "checkbox",
editor: ({ row }: any) =>
editor: ({ row, onRowChange, onClose }: any) =>
column.fieldType.type === "select" ? (
<Box pl="0.5rem">
<Select
variant="unstyled"
autoFocus
size="small"
value={row[column.key]}
onChange={updateSelect(row, column.key)}
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
onRowChange({ ...row, [column.key]: event.target.value }, true)
}}
placeholder=" "
>
{column.fieldType.options.map((option) => (
<option value={option.value}>{option.label}</option>
Expand All @@ -145,32 +222,59 @@ const TableComponent = connect(() => {
autoFocus
size="small"
value={row[column.key]}
onChange={updateInput(row, column.key)}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onRowChange({ ...row, [column.key]: event.target.value })
}}
onBlur={onClose}
/>
</Box>
),
editorOptions: {
editOnClick: true,
},
formatter: ({ row }: any) =>
formatter: ({ row, onRowChange }: any) =>
column.fieldType.type === "checkbox" ? (
<Box display="flex" alignItems="center" height="100%">
<Switch onChange={updateSwitch} />
<Box
display="flex"
alignItems="center"
height="100%"
onClick={(event) => {
event.stopPropagation()
}}
>
<Switch
isChecked={row[column.key]}
onChange={() => {
onRowChange({ ...row, [column.key]: !row[column.key] }, true)
}}
/>
</Box>
) : column.fieldType.type === "select" ? (
column.fieldType.options.find((option) => option.value === row[column.key])?.label
column.fieldType.options.find((option) => option.value === row[column.key])?.label || null
) : (
row[column.key]
),
cellClass: (row: { _errors: any[] | null; id: string }) =>
row._errors?.length && row._errors.find((err) => err.fieldName === column.key) ? "rdg-cell-error" : "",
cellClass: (row: any) => {
const index = state.data.indexOf(row)
switch (state.errors[index]?.[column.key]?.level) {
case "error":
return "rdg-cell-error"
case "warning":
return "rdg-cell-warning"
case "info":
return "rdg-cell-info"
default:
return ""
}
},
})),
]

return (
<EditableTable
rowKeyGetter={(row) => row.id}
rowKeyGetter={(row) => state.data.indexOf(row)}
rows={state.data}
onRowsChange={updateRow}
columns={columns}
selectedRows={state.selectedRows}
onSelectedRowsChange={(rows) => {
Expand Down
29 changes: 16 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export type Field = {
}

export type Checkbox = {
type: 'checkbox'
type: "checkbox"
// Alternate values to be treated as booleans, e.g. 'yes'-> true, 'no' -> false
booleanMatches?: {[key: string]: boolean}[]
booleanMatches?: { [key: string]: boolean }[]
}

export type Select = {
type : 'select'
type: "select"
// Options displayed in Select component
options: SelectOptions[]
}
Expand All @@ -58,24 +58,25 @@ export type SelectOptions = {
}

export type Input = {
type: 'input'
type: "input"
}

export type Validation = BasicValidation | RegexValidation
export type Validation = BasicValidation | RegexValidation

export type BasicValidation = {
rule: 'unique' | 'required' //... to be determined
rule: "unique" | "required" //... to be determined
errorMessage?: string
level?: ErrorLevel
}

export type RegexValidation = {
rule: 'regex'
regexOptions?: RegexOption[]
rule: "regex"
value: string
flags?: string
errorMessage: string
level?: ErrorLevel
}

export type RegexOption = 'i' | 'm' | 'u' | 's'

export type Hooks = {
// Runs after column matching and on entry change, more performant
rowHooks: RowHook[]
Expand All @@ -85,17 +86,19 @@ export type Hooks = {
initalHooks: TableHook[]
}

export type RowHook = ({...rowValues}: object) => Promise<Entry>
export type RowHook = ({ ...rowValues }: object) => Promise<Entry>
export type TableHook = (tableData: object[]) => Promise<Entry[]>

export type Entry = {
value: unknown
info: Info[]
}

export type ErrorLevel = "info" | "warning" | "error"

export type Info = {
message: string
level: 'info' | 'warning' | 'error'
level: ErrorLevel
}

export type Result = {
Expand All @@ -108,4 +111,4 @@ export type MaybeConfig = {
allowInvalidSubmit?: boolean
displayEncoding?: boolean
allowCustomFields?: boolean
}
}

0 comments on commit 2fb66f8

Please sign in to comment.