@commercetools-frontend-extensions/operations 2.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +138 -71
  3. package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +979 -143
  4. package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +979 -143
  5. package/dist/commercetools-frontend-extensions-operations.esm.js +929 -140
  6. package/dist/declarations/src/@api/file-import-jobs.d.ts +7 -0
  7. package/dist/declarations/src/@api/index.d.ts +1 -0
  8. package/dist/declarations/src/@api/test-fixtures.d.ts +8 -1
  9. package/dist/declarations/src/@api/urls.d.ts +30 -0
  10. package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +3 -2
  11. package/dist/declarations/src/@constants/file-import-job.d.ts +1 -0
  12. package/dist/declarations/src/@constants/import-limits.d.ts +6 -0
  13. package/dist/declarations/src/@constants/index.d.ts +2 -1
  14. package/dist/declarations/src/@errors/index.d.ts +5 -4
  15. package/dist/declarations/src/@errors/polling-aborted-error.d.ts +3 -0
  16. package/dist/declarations/src/@hooks/index.d.ts +4 -0
  17. package/dist/declarations/src/@hooks/use-fetch-file-import-job-records.d.ts +18 -0
  18. package/dist/declarations/src/@hooks/use-fetch-file-import-job.d.ts +17 -0
  19. package/dist/declarations/src/@hooks/use-file-import-job-upload.d.ts +18 -0
  20. package/dist/declarations/src/@hooks/use-file-upload.d.ts +29 -0
  21. package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +2 -1
  22. package/dist/declarations/src/@types/export-operation.d.ts +3 -1
  23. package/dist/declarations/src/@types/file-import-job.d.ts +99 -0
  24. package/dist/declarations/src/@types/file-upload-result.d.ts +21 -0
  25. package/dist/declarations/src/@types/file-upload.d.ts +2 -2
  26. package/dist/declarations/src/@types/index.d.ts +2 -0
  27. package/dist/declarations/src/@utils/file-import-job-helpers.d.ts +12 -0
  28. package/dist/declarations/src/@utils/file-upload.d.ts +8 -0
  29. package/dist/declarations/src/@utils/index.d.ts +2 -0
  30. package/dist/declarations/src/@utils/poll-job-until-validated.d.ts +11 -0
  31. package/package.json +12 -13
  32. package/src/@api/fetcher.ts +10 -0
  33. package/src/@api/file-import-jobs.ts +217 -0
  34. package/src/@api/file-upload.spec.ts +4 -2
  35. package/src/@api/index.ts +1 -0
  36. package/src/@api/test-fixtures.ts +127 -5
  37. package/src/@api/urls.ts +77 -1
  38. package/src/@components/uploading-modal/uploading-modal.tsx +7 -5
  39. package/src/@constants/file-import-job.ts +1 -0
  40. package/src/@constants/import-limits.ts +13 -0
  41. package/src/@constants/index.ts +2 -1
  42. package/src/@errors/index.ts +5 -4
  43. package/src/@errors/polling-aborted-error.ts +6 -0
  44. package/src/@hooks/index.ts +4 -0
  45. package/src/@hooks/use-fetch-file-import-job-records.ts +58 -0
  46. package/src/@hooks/use-fetch-file-import-job.spec.ts +131 -0
  47. package/src/@hooks/use-fetch-file-import-job.ts +38 -0
  48. package/src/@hooks/use-fetch.spec.ts +1 -9
  49. package/src/@hooks/use-fetch.ts +4 -8
  50. package/src/@hooks/use-file-import-job-upload.spec.ts +273 -0
  51. package/src/@hooks/use-file-import-job-upload.ts +101 -0
  52. package/src/@hooks/use-file-upload.ts +231 -0
  53. package/src/@hooks/use-import-container-upload.spec.ts +16 -13
  54. package/src/@hooks/use-import-container-upload.ts +6 -2
  55. package/src/@types/export-operation.ts +3 -0
  56. package/src/@types/file-import-job.ts +165 -0
  57. package/src/@types/file-upload-result.ts +23 -0
  58. package/src/@types/file-upload.ts +2 -2
  59. package/src/@types/index.ts +2 -0
  60. package/src/@utils/error-mapping.ts +10 -9
  61. package/src/@utils/file-import-job-helpers.spec.ts +147 -0
  62. package/src/@utils/file-import-job-helpers.ts +47 -0
  63. package/src/@utils/file-upload.ts +39 -0
  64. package/src/@utils/index.ts +2 -0
  65. package/src/@utils/poll-job-until-validated.ts +76 -0
  66. package/dist/declarations/src/@constants/upload-limits.d.ts +0 -10
  67. package/src/@constants/upload-limits.ts +0 -11
  68. package/src/@hooks/messages.ts +0 -11
@@ -0,0 +1,231 @@
1
+ import React from 'react'
2
+ import type { ResourceTypeId } from '@commercetools/importapi-sdk'
3
+ import { useImportContainerUpload } from './use-import-container-upload'
4
+ import { useFileImportJobUpload } from './use-file-import-job-upload'
5
+ import { deleteImportContainer } from '../@api'
6
+ import { HttpError, PollingAbortedError } from '../@errors'
7
+ import { pollJobUntilValidated, countUniqueResourcesInCsv } from '../@utils'
8
+ import type {
9
+ ExtendedImportContainerDraft,
10
+ FileUploadResult,
11
+ FileImportJob,
12
+ } from '../@types'
13
+
14
+ export type ValidationProgress = {
15
+ processed: number
16
+ total: number
17
+ isValidating: boolean
18
+ }
19
+
20
+ export type FileUploadConfig = {
21
+ file: File
22
+ resourceType: ResourceTypeId
23
+ settings?: ExtendedImportContainerDraft['settings']
24
+ onSuccess: (result: FileUploadResult) => void
25
+ onError?: (error: unknown) => void
26
+ onProgress?: (progress: number) => void
27
+ onValidationProgress?: (job: FileImportJob) => void
28
+ abortSignal?: AbortSignal
29
+ }
30
+
31
+ export type FileUploadOptions = {
32
+ projectKey: string
33
+ useJobBasedFlow?: boolean
34
+ pollingInterval?: number
35
+ maxPollingAttempts?: number
36
+ }
37
+
38
+ const safeDeleteContainer = async ({
39
+ projectKey,
40
+ containerKey,
41
+ }: {
42
+ projectKey: string
43
+ containerKey: string
44
+ }) => {
45
+ try {
46
+ await deleteImportContainer({
47
+ projectKey,
48
+ importContainerKey: containerKey,
49
+ })
50
+ } catch {}
51
+ }
52
+
53
+ export const useFileUpload = ({
54
+ projectKey,
55
+ useJobBasedFlow = false,
56
+ pollingInterval = 5000,
57
+ maxPollingAttempts = 120,
58
+ }: FileUploadOptions) => {
59
+ const [isUploading, setIsUploading] = React.useState(false)
60
+ const [progress, setProgress] = React.useState(0)
61
+ const [validationProgress, setValidationProgress] =
62
+ React.useState<ValidationProgress>({
63
+ processed: 0,
64
+ total: 0,
65
+ isValidating: false,
66
+ })
67
+
68
+ const containerUpload = useImportContainerUpload({ projectKey })
69
+ const jobUpload = useFileImportJobUpload({ projectKey })
70
+
71
+ const resetState = React.useCallback(() => {
72
+ setIsUploading(false)
73
+ setProgress(0)
74
+ setValidationProgress({ processed: 0, total: 0, isValidating: false })
75
+ }, [])
76
+
77
+ const upload = React.useCallback(
78
+ async (config: FileUploadConfig) => {
79
+ setIsUploading(true)
80
+ setProgress(0)
81
+
82
+ try {
83
+ if (useJobBasedFlow) {
84
+ const totalResources = await countUniqueResourcesInCsv(config.file)
85
+
86
+ await jobUpload.upload({
87
+ file: config.file,
88
+ resourceType: config.resourceType,
89
+ settings: config.settings,
90
+ abortSignal: config.abortSignal,
91
+ onSuccess: async (jobId, containerKey) => {
92
+ try {
93
+ setValidationProgress({
94
+ processed: 0,
95
+ total: totalResources,
96
+ isValidating: true,
97
+ })
98
+
99
+ const validatedJob = await pollJobUntilValidated({
100
+ projectKey,
101
+ jobId,
102
+ importContainerKey: containerKey,
103
+ pollingInterval,
104
+ maxAttempts: maxPollingAttempts,
105
+ abortSignal: config.abortSignal,
106
+ onJobUpdate: (job) => {
107
+ const processed = job.summary?.total ?? 0
108
+ setValidationProgress({
109
+ processed,
110
+ total: totalResources,
111
+ isValidating: true,
112
+ })
113
+ config.onValidationProgress?.(job)
114
+ },
115
+ })
116
+
117
+ // Handle rejected job with jobError for the new flow (like MissingCsvFieldIdentifier error)
118
+ // We wrap it in HttpError to reuse existing error handling (for the old flow) in consumers until BE supports this error type
119
+ if (validatedJob.jobError) {
120
+ throw new HttpError(
121
+ 400,
122
+ validatedJob.jobError.message,
123
+ validatedJob.jobError
124
+ )
125
+ }
126
+
127
+ if (validatedJob.summary.invalid > 0) {
128
+ await safeDeleteContainer({ projectKey, containerKey })
129
+ }
130
+
131
+ const result: FileUploadResult = {
132
+ containerKey,
133
+ summary: {
134
+ ...validatedJob.summary,
135
+ // TODO: Remove this once the old flow is fully removed
136
+ results: [],
137
+ },
138
+ jobId,
139
+ job: validatedJob,
140
+ }
141
+
142
+ setIsUploading(false)
143
+ setValidationProgress({
144
+ processed: 0,
145
+ total: 0,
146
+ isValidating: false,
147
+ })
148
+ config.onSuccess(result)
149
+ } catch (error) {
150
+ await safeDeleteContainer({ projectKey, containerKey })
151
+ resetState()
152
+
153
+ if (!(error instanceof PollingAbortedError)) {
154
+ config.onError?.(error)
155
+ }
156
+ }
157
+ },
158
+ onProgress: (prog) => {
159
+ setProgress(prog)
160
+ config.onProgress?.(prog)
161
+ },
162
+ onError: (error) => {
163
+ resetState()
164
+ config.onError?.(error)
165
+ },
166
+ })
167
+ } else {
168
+ await containerUpload.upload({
169
+ file: config.file,
170
+ resourceType: config.resourceType,
171
+ settings: config.settings,
172
+ abortSignal: config.abortSignal,
173
+ onSuccess: async (fileUploadResponse, containerKey) => {
174
+ if (config.abortSignal?.aborted) {
175
+ await safeDeleteContainer({ projectKey, containerKey })
176
+ resetState()
177
+ return
178
+ }
179
+
180
+ if (fileUploadResponse.invalid > 0) {
181
+ await safeDeleteContainer({ projectKey, containerKey })
182
+ }
183
+
184
+ const result: FileUploadResult = {
185
+ containerKey,
186
+ summary: {
187
+ total: fileUploadResponse.itemsCount,
188
+ valid: fileUploadResponse.valid,
189
+ invalid: fileUploadResponse.invalid,
190
+ fieldsCount: fileUploadResponse.columnsCount,
191
+ fields: fileUploadResponse.fields || [],
192
+ ignoredFields: fileUploadResponse.ignoredFields || [],
193
+ results: fileUploadResponse.results || [],
194
+ },
195
+ }
196
+
197
+ setIsUploading(false)
198
+ config.onSuccess(result)
199
+ },
200
+ onProgress: (prog) => {
201
+ setProgress(prog)
202
+ config.onProgress?.(prog)
203
+ },
204
+ onError: (error) => {
205
+ resetState()
206
+ config.onError?.(error)
207
+ },
208
+ })
209
+ }
210
+ } catch (error) {
211
+ resetState()
212
+ config.onError?.(error)
213
+ }
214
+ },
215
+ [
216
+ projectKey,
217
+ useJobBasedFlow,
218
+ pollingInterval,
219
+ maxPollingAttempts,
220
+ containerUpload,
221
+ jobUpload,
222
+ ]
223
+ )
224
+
225
+ return {
226
+ upload,
227
+ isUploading,
228
+ progress,
229
+ validationProgress,
230
+ }
231
+ }
@@ -42,8 +42,8 @@ describe('useImportContainerUpload', () => {
42
42
  itemsCount: 10,
43
43
  rowsCount: 10,
44
44
  columnsCount: 2,
45
- columns: ['key', 'value'],
46
- ignoredColumns: [],
45
+ fields: ['key', 'value'],
46
+ ignoredFields: [],
47
47
  }
48
48
 
49
49
  beforeEach(() => {
@@ -153,6 +153,7 @@ describe('useImportContainerUpload', () => {
153
153
  })
154
154
 
155
155
  it('should delete container if upload throws before starting', async () => {
156
+ const onError = jest.fn()
156
157
  const error = new Error('Container creation failed')
157
158
  mockCreateImportContainerForFileUpload.mockRejectedValue(error)
158
159
 
@@ -160,20 +161,22 @@ describe('useImportContainerUpload', () => {
160
161
  useImportContainerUpload({ projectKey })
161
162
  )
162
163
 
163
- await expect(
164
- act(async () => {
165
- await result.current.upload({
166
- file: mockFile,
167
- resourceType: 'product',
168
- onSuccess: jest.fn(),
169
- })
164
+ await act(async () => {
165
+ await result.current.upload({
166
+ file: mockFile,
167
+ resourceType: 'product',
168
+ onSuccess: jest.fn(),
169
+ onError,
170
170
  })
171
- ).rejects.toThrow(error)
171
+ })
172
172
 
173
- expect(mockDeleteImportContainer).toHaveBeenCalledWith({
174
- projectKey,
175
- importContainerKey,
173
+ await waitFor(() => {
174
+ expect(mockDeleteImportContainer).toHaveBeenCalledWith({
175
+ projectKey,
176
+ importContainerKey,
177
+ })
176
178
  })
179
+ expect(onError).toHaveBeenCalledWith(error)
177
180
  })
178
181
 
179
182
  it('should update progress during upload', async () => {
@@ -20,6 +20,7 @@ export type UseImportContainerUploadConfig = {
20
20
  onSuccess: (response: FileUploadResponse, importContainerKey: string) => void
21
21
  onError?: (error: unknown) => void
22
22
  onProgress?: (progress: number) => void
23
+ abortSignal?: AbortSignal
23
24
  }
24
25
 
25
26
  export const useImportContainerUpload = ({
@@ -38,7 +39,8 @@ export const useImportContainerUpload = ({
38
39
  onSuccess,
39
40
  onError,
40
41
  onProgress,
41
- }: UseImportContainerUploadConfig): Promise<XMLHttpRequest> => {
42
+ abortSignal,
43
+ }: UseImportContainerUploadConfig): Promise<XMLHttpRequest | undefined> => {
42
44
  if (!projectKey) {
43
45
  throw new ProjectKeyNotAvailableError()
44
46
  }
@@ -66,6 +68,7 @@ export const useImportContainerUpload = ({
66
68
  importContainerKey,
67
69
  resourceType,
68
70
  file,
71
+ abortSignal,
69
72
  onSuccess: (response) => {
70
73
  setIsUploading(false)
71
74
  setProgress(100)
@@ -107,7 +110,8 @@ export const useImportContainerUpload = ({
107
110
 
108
111
  setIsUploading(false)
109
112
  setProgress(0)
110
- throw error
113
+ onError?.(error)
114
+ return undefined
111
115
  }
112
116
  }
113
117
 
@@ -136,9 +136,12 @@ export type ValidationErrorCode =
136
136
  | 'AttributeDefinitionNotFound'
137
137
  | 'LocalizedFieldWithoutLocale'
138
138
  | 'IncompleteField'
139
+ | 'AttributeLevelMismatch'
139
140
 
140
141
  export type ValidationError = {
141
142
  code: ValidationErrorCode
142
143
  message: string
143
144
  field?: string
145
+ requestedLevel?: string
146
+ actualLevel?: string
144
147
  }
@@ -0,0 +1,165 @@
1
+ import { hasRequiredFields } from './shared'
2
+
3
+ export type FileImportJobState =
4
+ | 'queued'
5
+ | 'processing'
6
+ | 'validated'
7
+ | 'initialising'
8
+ | 'ready'
9
+ | 'rejected'
10
+
11
+ export interface FileImportJobSummary {
12
+ total: number
13
+ invalid: number
14
+ valid: number
15
+ fieldsCount: number
16
+ fields: string[]
17
+ ignoredFields: string[]
18
+ }
19
+
20
+ export interface FileImportJobValidationError {
21
+ code: string
22
+ message: string
23
+ rowValue?: Record<string, string>
24
+ metadata?: {
25
+ row?: number
26
+ }
27
+ }
28
+
29
+ export interface FileImportJob {
30
+ id: string
31
+ fileName: string
32
+ importContainerKey: string
33
+ state: FileImportJobState
34
+ summary: FileImportJobSummary
35
+ jobError?: FileImportJobValidationError | null
36
+ }
37
+
38
+ export interface CreateFileImportJobWithStreamPayload {
39
+ fileType: 'csv' | 'json'
40
+ fileName: string
41
+ file: File
42
+ }
43
+
44
+ export interface CreateFileImportJobWithReferencePayload {
45
+ payloadType: 'fileReference'
46
+ fileName: string
47
+ fileUrl: string
48
+ }
49
+
50
+ export type CreateFileImportJobPayload =
51
+ | CreateFileImportJobWithStreamPayload
52
+ | CreateFileImportJobWithReferencePayload
53
+
54
+ export interface CreateFileImportJobParameters {
55
+ projectKey: string
56
+ resourceType: string
57
+ importContainerKey: string
58
+ payload: CreateFileImportJobPayload
59
+ onProgress?: (progress: number) => void
60
+ abortSignal?: AbortSignal
61
+ }
62
+
63
+ export interface GetFileImportJobParameters {
64
+ projectKey: string
65
+ importContainerKey: string
66
+ jobId: string
67
+ }
68
+
69
+ export interface FileImportJobError {
70
+ code: string
71
+ message: string
72
+ field: string
73
+ }
74
+
75
+ export interface FileImportJobErrorRecord {
76
+ index: number
77
+ errors: FileImportJobError[]
78
+ }
79
+
80
+ export interface FileImportJobRecordsResponse {
81
+ results: FileImportJobErrorRecord[]
82
+ total: number
83
+ limit: number
84
+ offset: number
85
+ count: number
86
+ }
87
+
88
+ export interface GetFileImportJobRecordsParameters {
89
+ projectKey: string
90
+ importContainerKey: string
91
+ jobId: string
92
+ limit?: number
93
+ offset?: number
94
+ isValid?: boolean
95
+ }
96
+
97
+ export interface ProcessFileImportJobParameters {
98
+ projectKey: string
99
+ resourceType: string
100
+ importContainerKey: string
101
+ jobId: string
102
+ action?: 'delete'
103
+ }
104
+
105
+ export interface ProcessFileImportJobResponse {
106
+ message: string
107
+ }
108
+
109
+ export interface DeleteFileImportJobParameters {
110
+ projectKey: string
111
+ importContainerKey: string
112
+ jobId: string
113
+ }
114
+
115
+ export interface ListFileImportJobsParameters {
116
+ projectKey: string
117
+ importContainerKey: string
118
+ limit?: number
119
+ offset?: number
120
+ }
121
+
122
+ export type ListFileImportJobsResponse = FileImportJob[]
123
+
124
+ export function assertFileImportJob(
125
+ maybeJob: unknown
126
+ ): asserts maybeJob is FileImportJob {
127
+ const requiredFields = ['id', 'fileName', 'importContainerKey', 'state']
128
+ if (hasRequiredFields(maybeJob, requiredFields)) return
129
+ throw new Error('Invalid File Import Job response')
130
+ }
131
+
132
+ export function assertFileImportJobRecordsResponse(
133
+ maybeRecords: unknown
134
+ ): asserts maybeRecords is FileImportJobRecordsResponse {
135
+ const requiredFields = ['results', 'total', 'limit', 'offset', 'count']
136
+ if (!hasRequiredFields(maybeRecords, requiredFields)) {
137
+ throw new Error(
138
+ 'Invalid File Import Job records response: missing required fields'
139
+ )
140
+ }
141
+ }
142
+
143
+ export function assertProcessFileImportJobResponse(
144
+ maybeResponse: unknown
145
+ ): asserts maybeResponse is ProcessFileImportJobResponse {
146
+ const requiredFields = ['message']
147
+ if (hasRequiredFields(maybeResponse, requiredFields)) return
148
+ throw new Error('Invalid Process File Import Job response')
149
+ }
150
+
151
+ export function assertListFileImportJobsResponse(
152
+ maybeResponse: unknown
153
+ ): asserts maybeResponse is ListFileImportJobsResponse {
154
+ if (!Array.isArray(maybeResponse)) {
155
+ throw new Error('Invalid List File Import Jobs response: expected an array')
156
+ }
157
+ if (maybeResponse.length > 0) {
158
+ const requiredFields = ['id', 'fileName', 'importContainerKey', 'state']
159
+ if (!hasRequiredFields(maybeResponse[0], requiredFields)) {
160
+ throw new Error(
161
+ 'Invalid List File Import Jobs response: missing required fields'
162
+ )
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,23 @@
1
+ import type { FileImportJob } from './file-import-job'
2
+ import type { RowErrorsResponse } from './file-upload'
3
+
4
+ export interface FileUploadResult {
5
+ containerKey: string
6
+ summary: {
7
+ total: number
8
+ valid: number
9
+ invalid: number
10
+ fieldsCount: number
11
+ fields: string[]
12
+ ignoredFields: string[]
13
+ /**
14
+ * Validation errors. Format is compatible between old and new flows:
15
+ * - Old flow: { row?: number, index?: number, errors: RowError[] }
16
+ * - New flow: { index: number, errors: FileImportJobError[] }
17
+ */
18
+ results: Array<RowErrorsResponse>
19
+ }
20
+ // new job based flow-specific fields
21
+ jobId?: string
22
+ job?: FileImportJob
23
+ }
@@ -34,8 +34,8 @@ export type FileUploadResponse = {
34
34
  itemsCount: number
35
35
  rowsCount: number
36
36
  columnsCount: number
37
- columns: Array<string>
38
- ignoredColumns: Array<string>
37
+ fields: Array<string>
38
+ ignoredFields: Array<string>
39
39
  }
40
40
 
41
41
  export interface FileUploadRequestParameters {
@@ -2,6 +2,8 @@ export * from './api'
2
2
  export * from './basic-error-data-type'
3
3
  export * from './export-operation'
4
4
  export * from './file-upload'
5
+ export * from './file-upload-result'
6
+ export * from './file-import-job'
5
7
  export * from './import-container'
6
8
  export * from './import-operation'
7
9
  export * from './import-states'
@@ -5,6 +5,7 @@ export function getFileUploadErrorsCount(
5
5
  return errors.reduce((acc, curr) => (acc += curr.errors.length), 0)
6
6
  }
7
7
 
8
+ // TODO: After fully migrating to new flow, remove `row` and only use `index`
8
9
  export function mapUploadFileErrorsResponseToUploadFileErrorRows(
9
10
  uploadFileErrorsResponse?: Array<{
10
11
  row?: number
@@ -22,18 +23,18 @@ export function mapUploadFileErrorsResponseToUploadFileErrorRows(
22
23
  if (!uploadFileErrorsResponse || !Array.isArray(uploadFileErrorsResponse))
23
24
  return []
24
25
  let idCounter = 1
25
- return uploadFileErrorsResponse.flatMap((rowErrorsResponse) =>
26
- rowErrorsResponse.errors.map((rowError) => ({
26
+ return uploadFileErrorsResponse.flatMap((rowErrorsResponse) => {
27
+ // TODO: use `row` if available, otherwise fall back to `index`
28
+ // Old flow uses `row`, new flow uses `index`
29
+ const rowNumber = rowErrorsResponse.row ?? rowErrorsResponse.index
30
+
31
+ return rowErrorsResponse.errors.map((rowError) => ({
27
32
  id: String(idCounter++),
28
- ...(rowErrorsResponse.row !== undefined
29
- ? { row: rowErrorsResponse.row }
30
- : {}),
31
- ...(rowErrorsResponse.index !== undefined
32
- ? { index: rowErrorsResponse.index }
33
- : {}),
33
+ row: rowNumber,
34
+ index: rowNumber,
34
35
  field: rowError.field,
35
36
  code: rowError.code,
36
37
  validationMessage: rowError.message,
37
38
  }))
38
- )
39
+ })
39
40
  }