@commercetools-frontend-extensions/operations 0.0.0 → 3.0.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.
- package/CHANGELOG.md +6 -0
- package/README.md +81 -72
- package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +947 -143
- package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +947 -143
- package/dist/commercetools-frontend-extensions-operations.esm.js +898 -140
- package/dist/declarations/src/@api/file-import-jobs.d.ts +7 -0
- package/dist/declarations/src/@api/index.d.ts +1 -0
- package/dist/declarations/src/@api/test-fixtures.d.ts +8 -1
- package/dist/declarations/src/@api/urls.d.ts +30 -0
- package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +3 -2
- package/dist/declarations/src/@constants/file-import-job.d.ts +1 -0
- package/dist/declarations/src/@constants/import-limits.d.ts +6 -0
- package/dist/declarations/src/@constants/index.d.ts +2 -1
- package/dist/declarations/src/@errors/index.d.ts +5 -4
- package/dist/declarations/src/@errors/polling-aborted-error.d.ts +3 -0
- package/dist/declarations/src/@hooks/index.d.ts +3 -0
- package/dist/declarations/src/@hooks/use-fetch-file-import-job.d.ts +17 -0
- package/dist/declarations/src/@hooks/use-file-import-job-upload.d.ts +18 -0
- package/dist/declarations/src/@hooks/use-file-upload.d.ts +28 -0
- package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +2 -1
- package/dist/declarations/src/@types/export-operation.d.ts +3 -1
- package/dist/declarations/src/@types/file-import-job.d.ts +99 -0
- package/dist/declarations/src/@types/file-upload-result.d.ts +21 -0
- package/dist/declarations/src/@types/file-upload.d.ts +2 -2
- package/dist/declarations/src/@types/index.d.ts +2 -0
- package/dist/declarations/src/@utils/file-import-job-helpers.d.ts +12 -0
- package/dist/declarations/src/@utils/file-upload.d.ts +8 -0
- package/dist/declarations/src/@utils/index.d.ts +2 -0
- package/dist/declarations/src/@utils/poll-job-until-validated.d.ts +11 -0
- package/package.json +19 -20
- package/src/@api/fetcher.ts +10 -0
- package/src/@api/file-import-jobs.ts +217 -0
- package/src/@api/file-upload.spec.ts +4 -2
- package/src/@api/index.ts +1 -0
- package/src/@api/test-fixtures.ts +127 -5
- package/src/@api/urls.ts +77 -1
- package/src/@components/uploading-modal/uploading-modal.tsx +7 -5
- package/src/@constants/file-import-job.ts +1 -0
- package/src/@constants/import-limits.ts +13 -0
- package/src/@constants/index.ts +2 -1
- package/src/@errors/index.ts +5 -4
- package/src/@errors/polling-aborted-error.ts +6 -0
- package/src/@hooks/index.ts +3 -0
- package/src/@hooks/use-fetch-file-import-job.spec.ts +131 -0
- package/src/@hooks/use-fetch-file-import-job.ts +38 -0
- package/src/@hooks/use-fetch.spec.ts +1 -9
- package/src/@hooks/use-fetch.ts +4 -8
- package/src/@hooks/use-file-import-job-upload.spec.ts +273 -0
- package/src/@hooks/use-file-import-job-upload.ts +101 -0
- package/src/@hooks/use-file-upload.ts +223 -0
- package/src/@hooks/use-import-container-upload.spec.ts +16 -13
- package/src/@hooks/use-import-container-upload.ts +6 -2
- package/src/@types/export-operation.ts +3 -0
- package/src/@types/file-import-job.ts +165 -0
- package/src/@types/file-upload-result.ts +23 -0
- package/src/@types/file-upload.ts +2 -2
- package/src/@types/index.ts +2 -0
- package/src/@utils/error-mapping.ts +10 -9
- package/src/@utils/file-import-job-helpers.spec.ts +147 -0
- package/src/@utils/file-import-job-helpers.ts +47 -0
- package/src/@utils/file-upload.ts +39 -0
- package/src/@utils/index.ts +2 -0
- package/src/@utils/poll-job-until-validated.ts +76 -0
- package/dist/declarations/src/@constants/upload-limits.d.ts +0 -10
- package/src/@constants/upload-limits.ts +0 -11
- package/src/@hooks/messages.ts +0 -11
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
2
|
+
import type { FileImportJob } from '../@types'
|
|
3
|
+
import {
|
|
4
|
+
getFileImportJobFileType,
|
|
5
|
+
isImportJobQueued,
|
|
6
|
+
isImportJobProcessing,
|
|
7
|
+
isImportJobValidated,
|
|
8
|
+
isImportJobInitializing,
|
|
9
|
+
isImportJobReady,
|
|
10
|
+
shouldContinuePollingForImportValidation,
|
|
11
|
+
} from './file-import-job-helpers'
|
|
12
|
+
|
|
13
|
+
describe('file-import-job-helpers', () => {
|
|
14
|
+
describe('getFileImportJobFileType', () => {
|
|
15
|
+
it('should return "json" for custom-object resource type', () => {
|
|
16
|
+
expect(getFileImportJobFileType('custom-object' as ResourceTypeId)).toBe(
|
|
17
|
+
'json'
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should return "csv" for product resource type', () => {
|
|
22
|
+
expect(getFileImportJobFileType('product' as ResourceTypeId)).toBe('csv')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should return "csv" for category resource type', () => {
|
|
26
|
+
expect(getFileImportJobFileType('category' as ResourceTypeId)).toBe('csv')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should return "csv" for other resource types', () => {
|
|
30
|
+
expect(getFileImportJobFileType('price' as ResourceTypeId)).toBe('csv')
|
|
31
|
+
expect(
|
|
32
|
+
getFileImportJobFileType('inventory-entry' as ResourceTypeId)
|
|
33
|
+
).toBe('csv')
|
|
34
|
+
expect(getFileImportJobFileType('customer' as ResourceTypeId)).toBe('csv')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const mockJob: FileImportJob = {
|
|
39
|
+
id: 'test-job-id',
|
|
40
|
+
fileName: 'test.csv',
|
|
41
|
+
importContainerKey: 'test-container',
|
|
42
|
+
state: 'validated',
|
|
43
|
+
summary: {
|
|
44
|
+
total: 100,
|
|
45
|
+
invalid: 5,
|
|
46
|
+
valid: 95,
|
|
47
|
+
fieldsCount: 10,
|
|
48
|
+
fields: ['field1', 'field2'],
|
|
49
|
+
ignoredFields: ['ignored1'],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('isImportJobQueued', () => {
|
|
54
|
+
it('should return true when job is queued', () => {
|
|
55
|
+
const queuedJob = { ...mockJob, state: 'queued' as const }
|
|
56
|
+
expect(isImportJobQueued(queuedJob)).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should return false when job is not queued', () => {
|
|
60
|
+
expect(isImportJobQueued(mockJob)).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should return false when job is undefined', () => {
|
|
64
|
+
expect(isImportJobQueued(undefined)).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('isImportJobProcessing', () => {
|
|
69
|
+
it('should return true when job is processing', () => {
|
|
70
|
+
const processingJob = { ...mockJob, state: 'processing' as const }
|
|
71
|
+
expect(isImportJobProcessing(processingJob)).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should return false when job is not processing', () => {
|
|
75
|
+
expect(isImportJobProcessing(mockJob)).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('isImportJobValidated', () => {
|
|
80
|
+
it('should return true when job is validated', () => {
|
|
81
|
+
expect(isImportJobValidated(mockJob)).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should return false when job is not validated', () => {
|
|
85
|
+
const processingJob = { ...mockJob, state: 'processing' as const }
|
|
86
|
+
expect(isImportJobValidated(processingJob)).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should return false when job is undefined', () => {
|
|
90
|
+
expect(isImportJobValidated(undefined)).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('isImportJobInitializing', () => {
|
|
95
|
+
it('should return true when job is initialising', () => {
|
|
96
|
+
const initialisingJob = { ...mockJob, state: 'initialising' as const }
|
|
97
|
+
expect(isImportJobInitializing(initialisingJob)).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should return false when job is not initialising', () => {
|
|
101
|
+
expect(isImportJobInitializing(mockJob)).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('isImportJobReady', () => {
|
|
106
|
+
it('should return true when job is ready', () => {
|
|
107
|
+
const readyJob = { ...mockJob, state: 'ready' as const }
|
|
108
|
+
expect(isImportJobReady(readyJob)).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should return false when job is not ready', () => {
|
|
112
|
+
expect(isImportJobReady(mockJob)).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('shouldContinuePollingForImportValidation', () => {
|
|
117
|
+
it('should return true when job is undefined', () => {
|
|
118
|
+
expect(shouldContinuePollingForImportValidation(undefined)).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should return true when job is queued', () => {
|
|
122
|
+
const queuedJob = { ...mockJob, state: 'queued' as const }
|
|
123
|
+
expect(shouldContinuePollingForImportValidation(queuedJob)).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should return true when job is processing', () => {
|
|
127
|
+
const processingJob = { ...mockJob, state: 'processing' as const }
|
|
128
|
+
expect(shouldContinuePollingForImportValidation(processingJob)).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should return false when job is validated', () => {
|
|
132
|
+
expect(shouldContinuePollingForImportValidation(mockJob)).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should return false when job is initialising', () => {
|
|
136
|
+
const initialisingJob = { ...mockJob, state: 'initialising' as const }
|
|
137
|
+
expect(shouldContinuePollingForImportValidation(initialisingJob)).toBe(
|
|
138
|
+
false
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should return false when job is ready', () => {
|
|
143
|
+
const readyJob = { ...mockJob, state: 'ready' as const }
|
|
144
|
+
expect(shouldContinuePollingForImportValidation(readyJob)).toBe(false)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
2
|
+
import type { FileImportJob } from '../@types'
|
|
3
|
+
|
|
4
|
+
export function getFileImportJobFileType(
|
|
5
|
+
resourceType: ResourceTypeId
|
|
6
|
+
): 'csv' | 'json' {
|
|
7
|
+
return resourceType === 'custom-object' ? 'json' : 'csv'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toImportApiResourceType(resourceType: string): string {
|
|
11
|
+
return resourceType === 'product' ? 'product-draft' : resourceType
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isImportJobQueued(job?: FileImportJob): boolean {
|
|
15
|
+
return job?.state === 'queued'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isImportJobProcessing(job?: FileImportJob): boolean {
|
|
19
|
+
return job?.state === 'processing'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isImportJobValidated(job?: FileImportJob): boolean {
|
|
23
|
+
return job?.state === 'validated'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isImportJobInitializing(job?: FileImportJob): boolean {
|
|
27
|
+
return job?.state === 'initialising'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isImportJobReady(job?: FileImportJob): boolean {
|
|
31
|
+
return job?.state === 'ready'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isImportJobRejected(job?: FileImportJob): boolean {
|
|
35
|
+
return job?.state === 'rejected'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isImportJobTerminal(job?: FileImportJob): boolean {
|
|
39
|
+
return isImportJobValidated(job) || isImportJobRejected(job)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function shouldContinuePollingForImportValidation(
|
|
43
|
+
job?: FileImportJob
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!job) return true
|
|
46
|
+
return isImportJobQueued(job) || isImportJobProcessing(job)
|
|
47
|
+
}
|
|
@@ -131,6 +131,45 @@ export const countJsonFileItems = async (
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Count unique resources in a CSV file by counting unique values in the "key" column.
|
|
136
|
+
* A single resource can span multiple rows (when it has array fields like variants, assets...),
|
|
137
|
+
* so we count unique keys rather than rows.
|
|
138
|
+
* @param file The CSV file to process
|
|
139
|
+
* @returns A promise that resolves to the number of unique resources
|
|
140
|
+
*/
|
|
141
|
+
export const countUniqueResourcesInCsv = (file: File): Promise<number> => {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const uniqueKeys = new Set<string>()
|
|
144
|
+
let keyColumnIndex = -1
|
|
145
|
+
let isFirstRow = true
|
|
146
|
+
|
|
147
|
+
Papa.parse<string[]>(file, {
|
|
148
|
+
step: ({ data }) => {
|
|
149
|
+
if (!Array.isArray(data)) return
|
|
150
|
+
|
|
151
|
+
if (isFirstRow) {
|
|
152
|
+
keyColumnIndex = data.findIndex(
|
|
153
|
+
(col) => col?.toLowerCase().trim() === 'key'
|
|
154
|
+
)
|
|
155
|
+
isFirstRow = false
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (keyColumnIndex === -1) return
|
|
160
|
+
|
|
161
|
+
const keyValue = data[keyColumnIndex]?.trim()
|
|
162
|
+
if (keyValue) {
|
|
163
|
+
uniqueKeys.add(keyValue)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
complete: () => {
|
|
167
|
+
resolve(uniqueKeys.size)
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
134
173
|
/**
|
|
135
174
|
* Map file upload errors to upload file error rows with unique IDs
|
|
136
175
|
* @param uploadFileErrors Array of file upload errors
|
package/src/@utils/index.ts
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getFileImportJob } from '../@api'
|
|
2
|
+
import { PollingAbortedError } from '../@errors'
|
|
3
|
+
import type { FileImportJob } from '../@types'
|
|
4
|
+
import { isImportJobTerminal } from './file-import-job-helpers'
|
|
5
|
+
|
|
6
|
+
export type PollJobUntilValidatedConfig = {
|
|
7
|
+
projectKey: string
|
|
8
|
+
jobId: string
|
|
9
|
+
importContainerKey: string
|
|
10
|
+
pollingInterval?: number
|
|
11
|
+
maxAttempts?: number
|
|
12
|
+
onJobUpdate?: (job: FileImportJob) => void
|
|
13
|
+
abortSignal?: AbortSignal
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const pollJobUntilValidated = async ({
|
|
17
|
+
projectKey,
|
|
18
|
+
jobId,
|
|
19
|
+
importContainerKey,
|
|
20
|
+
pollingInterval = 5000,
|
|
21
|
+
maxAttempts = 120,
|
|
22
|
+
onJobUpdate,
|
|
23
|
+
abortSignal,
|
|
24
|
+
}: PollJobUntilValidatedConfig): Promise<FileImportJob> => {
|
|
25
|
+
let attempts = 0
|
|
26
|
+
|
|
27
|
+
while (attempts < maxAttempts) {
|
|
28
|
+
if (abortSignal?.aborted) {
|
|
29
|
+
throw new PollingAbortedError()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const job = await getFileImportJob({
|
|
33
|
+
projectKey,
|
|
34
|
+
importContainerKey,
|
|
35
|
+
jobId,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (abortSignal?.aborted) {
|
|
39
|
+
throw new PollingAbortedError()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onJobUpdate?.(job)
|
|
43
|
+
|
|
44
|
+
if (isImportJobTerminal(job)) {
|
|
45
|
+
return job
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await new Promise<void>((resolve, reject) => {
|
|
49
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
50
|
+
|
|
51
|
+
const onAbort = () => {
|
|
52
|
+
clearTimeout(timeoutId)
|
|
53
|
+
reject(new PollingAbortedError())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (abortSignal?.aborted) {
|
|
57
|
+
reject(new PollingAbortedError())
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
timeoutId = setTimeout(() => {
|
|
62
|
+
abortSignal?.removeEventListener('abort', onAbort)
|
|
63
|
+
resolve()
|
|
64
|
+
}, pollingInterval)
|
|
65
|
+
|
|
66
|
+
abortSignal?.addEventListener('abort', onAbort)
|
|
67
|
+
})
|
|
68
|
+
attempts++
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Job validation timeout after ${maxAttempts} attempts (${
|
|
73
|
+
(maxAttempts * pollingInterval) / 1000
|
|
74
|
+
}s)`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maximum file size for imports.
|
|
3
|
-
* Recommended by backend, enforced in frontend validation.
|
|
4
|
-
*/
|
|
5
|
-
export declare const MAX_FILE_SIZE_MB = 35;
|
|
6
|
-
/**
|
|
7
|
-
* Maximum row count for imports.
|
|
8
|
-
* Recommended by backend, enforced in frontend validation.
|
|
9
|
-
*/
|
|
10
|
-
export declare const MAX_ROW_COUNT = 80000;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maximum file size for imports.
|
|
3
|
-
* Recommended by backend, enforced in frontend validation.
|
|
4
|
-
*/
|
|
5
|
-
export const MAX_FILE_SIZE_MB = 35
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Maximum row count for imports.
|
|
9
|
-
* Recommended by backend, enforced in frontend validation.
|
|
10
|
-
*/
|
|
11
|
-
export const MAX_ROW_COUNT = 80_000
|
package/src/@hooks/messages.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { defineMessages } from 'react-intl'
|
|
2
|
-
|
|
3
|
-
export default defineMessages({
|
|
4
|
-
unexpectedError: {
|
|
5
|
-
id: 'operations.fetch.unexpectedError',
|
|
6
|
-
description:
|
|
7
|
-
'Generic error message displayed when an unexpected error occurs during data fetching',
|
|
8
|
-
defaultMessage:
|
|
9
|
-
'An unexpected error occurred while fetching the data. Please try again. If the problem persists, please contact support.',
|
|
10
|
-
},
|
|
11
|
-
})
|