Skip to content

Commit e78502e

Browse files
committed
Fix error validation when validating only changed rows
1 parent df0675c commit e78502e

File tree

4 files changed

+85
-38
lines changed

4 files changed

+85
-38
lines changed

src/steps/ValidationStep/ValidationStep.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export const ValidationStep = <T extends string>({ initialData, file }: Props<T>
3030

3131
const updateData = useCallback(
3232
async (rows: typeof data, indexes?: number[]) => {
33+
// Check if hooks are async - if they are we want to apply changes optimistically for better UX
34+
if (rowHook?.constructor.name === "AsyncFunction" || tableHook?.constructor.name === "AsyncFunction") {
35+
setData(rows)
36+
}
3337
addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook, indexes).then((data) => setData(data))
3438
},
3539
[rowHook, tableHook, fields],

src/steps/ValidationStep/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Info } from "../../types"
1+
import { InfoWithSource } from "../../types"
22

33
export type Meta = { __index: string; __errors?: Error | null }
4-
export type Error = { [key: string]: Info }
4+
export type Error = { [key: string]: InfoWithSource }
55
export type Errors = { [id: string]: Error }
Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Data, Fields, Info, RowHook, TableHook } from "../../../types"
2-
import type { Meta, Errors } from "../types"
2+
import type { Meta, Error, Errors } from "../types"
33
import { v4 } from "uuid"
4+
import { ErrorSources } from "../../../types"
45

56
export const addErrorsAndRunHooks = async <T extends string>(
67
data: (Data<T> & Partial<Meta>)[],
@@ -11,25 +12,27 @@ export const addErrorsAndRunHooks = async <T extends string>(
1112
): Promise<(Data<T> & Meta)[]> => {
1213
const errors: Errors = {}
1314

14-
const addHookError = (rowIndex: number, fieldKey: T, error: Info) => {
15+
const addError = (source: ErrorSources, rowIndex: number, fieldKey: T, error: Info) => {
1516
errors[rowIndex] = {
1617
...errors[rowIndex],
17-
[fieldKey]: error,
18+
[fieldKey]: { ...error, source },
1819
}
1920
}
2021

2122
if (tableHook) {
22-
data = await tableHook(data, addHookError)
23+
data = await tableHook(data, (...props) => addError(ErrorSources.Table, ...props))
2324
}
2425

2526
if (rowHook) {
26-
if (changedRowIndexes != null) {
27+
if (changedRowIndexes) {
2728
for (const index of changedRowIndexes) {
28-
data[index] = await rowHook(data[index], (...props) => addHookError(index, ...props), data)
29+
data[index] = await rowHook(data[index], (...props) => addError(ErrorSources.Row, index, ...props), data)
2930
}
3031
} else {
3132
data = await Promise.all(
32-
data.map(async (value, index) => rowHook(value, (...props) => addHookError(index, ...props), data)),
33+
data.map(async (value, index) =>
34+
rowHook(value, (...props) => addError(ErrorSources.Row, index, ...props), data),
35+
),
3336
)
3437
}
3538
}
@@ -58,47 +61,43 @@ export const addErrorsAndRunHooks = async <T extends string>(
5861

5962
values.forEach((value, index) => {
6063
if (duplicates.has(value)) {
61-
errors[index] = {
62-
...errors[index],
63-
[field.key]: {
64-
level: validation.level || "error",
65-
message: validation.errorMessage || "Field must be unique",
66-
},
67-
}
64+
addError(ErrorSources.Table, index, field.key as T, {
65+
level: validation.level || "error",
66+
message: validation.errorMessage || "Field must be unique",
67+
})
6868
}
6969
})
7070
break
7171
}
7272
case "required": {
73-
data.forEach((entry, index) => {
73+
const dataToValidate = changedRowIndexes ? changedRowIndexes.map((index) => data[index]) : data
74+
dataToValidate.forEach((entry, index) => {
75+
const realIndex = changedRowIndexes ? changedRowIndexes[index] : index
7476
if (entry[field.key as T] === null || entry[field.key as T] === undefined || entry[field.key as T] === "") {
75-
errors[index] = {
76-
...errors[index],
77-
[field.key]: {
78-
level: validation.level || "error",
79-
message: validation.errorMessage || "Field is required",
80-
},
81-
}
77+
addError(ErrorSources.Row, realIndex, field.key as T, {
78+
level: validation.level || "error",
79+
message: validation.errorMessage || "Field is required",
80+
})
8281
}
8382
})
83+
8484
break
8585
}
8686
case "regex": {
87+
const dataToValidate = changedRowIndexes ? changedRowIndexes.map((index) => data[index]) : data
8788
const regex = new RegExp(validation.value, validation.flags)
88-
data.forEach((entry, index) => {
89+
dataToValidate.forEach((entry, index) => {
90+
const realIndex = changedRowIndexes ? changedRowIndexes[index] : index
8991
const value = entry[field.key]?.toString() ?? ""
9092
if (!value.match(regex)) {
91-
errors[index] = {
92-
...errors[index],
93-
[field.key]: {
94-
level: validation.level || "error",
95-
message:
96-
validation.errorMessage ||
97-
`Field did not match the regex /${validation.value}/${validation.flags} `,
98-
},
99-
}
93+
addError(ErrorSources.Row, realIndex, field.key as T, {
94+
level: validation.level || "error",
95+
message:
96+
validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `,
97+
})
10098
}
10199
})
100+
102101
break
103102
}
104103
}
@@ -112,12 +111,41 @@ export const addErrorsAndRunHooks = async <T extends string>(
112111
}
113112
const newValue = value as Data<T> & Meta
114113

115-
if (errors[index]) {
116-
return { ...newValue, __errors: errors[index] }
114+
// If we are validating all indexes, or we did full validation on this row - apply all errors
115+
if (!changedRowIndexes || changedRowIndexes.includes(index)) {
116+
if (errors[index]) {
117+
return { ...newValue, __errors: errors[index] }
118+
}
119+
120+
if (!errors[index] && value?.__errors) {
121+
return { ...newValue, __errors: null }
122+
}
117123
}
118-
if (!errors[index] && value?.__errors) {
119-
return { ...newValue, __errors: null }
124+
// if we have not validated this row, keep it's row errors but apply global error changes
125+
else {
126+
// at this point errors[index] contains only table source errors, previous row and table errors are in value.__errors
127+
const hasRowErrors =
128+
value.__errors && Object.values(value.__errors).some((error) => error.source === ErrorSources.Row)
129+
130+
if (!hasRowErrors) {
131+
if (errors[index]) {
132+
return { ...newValue, __errors: errors[index] }
133+
}
134+
return newValue
135+
}
136+
137+
const errorsWithoutTableError = Object.entries(value.__errors!).reduce((acc, [key, value]) => {
138+
if (value.source === ErrorSources.Row) {
139+
acc[key] = value
140+
}
141+
return acc
142+
}, {} as Error)
143+
144+
const newErrors = { ...errorsWithoutTableError, ...errors[index] }
145+
146+
return { ...newValue, __errors: newErrors }
120147
}
148+
121149
return newValue
122150
})
123151
}

src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ export type Info = {
134134
level: ErrorLevel
135135
}
136136

137+
export enum ErrorSources {
138+
Table = "table",
139+
Row = "row",
140+
}
141+
142+
/*
143+
Source determines whether the error is from the full table or row validation
144+
Table validation is tableHook and "unique" validation
145+
Row validation is rowHook and all other validations
146+
it is used to determine if row.__errors should be updated or not depending on different validations
147+
*/
148+
export type InfoWithSource = Info & {
149+
source: ErrorSources
150+
}
151+
137152
export type Result<T extends string> = {
138153
validData: Data<T>[]
139154
invalidData: Data<T>[]

0 commit comments

Comments
 (0)