@commercetools-frontend-extensions/operations 0.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 +55 -0
- package/README.md +169 -0
- package/babel.config.js +6 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.d.ts +2 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +2469 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.js +7 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +2461 -0
- package/dist/commercetools-frontend-extensions-operations.esm.js +2316 -0
- package/dist/declarations/src/@api/export-operations.d.ts +5 -0
- package/dist/declarations/src/@api/fetcher.d.ts +17 -0
- package/dist/declarations/src/@api/file-upload.d.ts +3 -0
- package/dist/declarations/src/@api/import-containers.d.ts +35 -0
- package/dist/declarations/src/@api/import-operations.d.ts +6 -0
- package/dist/declarations/src/@api/index.d.ts +8 -0
- package/dist/declarations/src/@api/process-file.d.ts +3 -0
- package/dist/declarations/src/@api/test-fixtures.d.ts +272 -0
- package/dist/declarations/src/@api/urls.d.ts +44 -0
- package/dist/declarations/src/@components/file-drop-area/active-drag-drop-area.d.ts +10 -0
- package/dist/declarations/src/@components/file-drop-area/disabled-drop-area.d.ts +5 -0
- package/dist/declarations/src/@components/file-drop-area/drop-area-wrapper.d.ts +11 -0
- package/dist/declarations/src/@components/file-drop-area/enabled-drop-area.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/file-drop-area.d.ts +14 -0
- package/dist/declarations/src/@components/file-drop-area/file-dropped-area.d.ts +6 -0
- package/dist/declarations/src/@components/file-drop-area/index.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/styles.d.ts +9 -0
- package/dist/declarations/src/@components/icons/file-icon.d.ts +2 -0
- package/dist/declarations/src/@components/icons/index.d.ts +2 -0
- package/dist/declarations/src/@components/icons/lock-icon.d.ts +2 -0
- package/dist/declarations/src/@components/index.d.ts +6 -0
- package/dist/declarations/src/@components/info-box/index.d.ts +1 -0
- package/dist/declarations/src/@components/info-box/info-box.d.ts +7 -0
- package/dist/declarations/src/@components/upload-separator/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-separator/upload-separator.d.ts +12 -0
- package/dist/declarations/src/@components/upload-settings/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-settings/upload-settings.d.ts +11 -0
- package/dist/declarations/src/@components/uploading-modal/index.d.ts +1 -0
- package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +12 -0
- package/dist/declarations/src/@constants/delimiters.d.ts +8 -0
- package/dist/declarations/src/@constants/import-tags.d.ts +7 -0
- package/dist/declarations/src/@constants/index.d.ts +4 -0
- package/dist/declarations/src/@constants/resource-links.d.ts +10 -0
- package/dist/declarations/src/@constants/upload-limits.d.ts +10 -0
- package/dist/declarations/src/@errors/http-error.d.ts +6 -0
- package/dist/declarations/src/@errors/index.d.ts +8 -0
- package/dist/declarations/src/@errors/invalid-response-error.d.ts +3 -0
- package/dist/declarations/src/@errors/no-resources-to-export-error.d.ts +3 -0
- package/dist/declarations/src/@errors/project-key-not-available-error.d.ts +3 -0
- package/dist/declarations/src/@errors/query-predicate-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-column-error.d.ts +3 -0
- package/dist/declarations/src/@errors/unexpected-operation-state-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-resource-type-error.d.ts +3 -0
- package/dist/declarations/src/@hooks/index.d.ts +5 -0
- package/dist/declarations/src/@hooks/use-fetch-export-operations.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-container-details.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-operations.d.ts +16 -0
- package/dist/declarations/src/@hooks/use-fetch-import-summaries.d.ts +20 -0
- package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +18 -0
- package/dist/declarations/src/@types/api.d.ts +13 -0
- package/dist/declarations/src/@types/basic-error-data-type.d.ts +5 -0
- package/dist/declarations/src/@types/export-operation.d.ts +95 -0
- package/dist/declarations/src/@types/file-upload.d.ts +63 -0
- package/dist/declarations/src/@types/import-container.d.ts +53 -0
- package/dist/declarations/src/@types/import-operation.d.ts +13 -0
- package/dist/declarations/src/@types/import-states.d.ts +9 -0
- package/dist/declarations/src/@types/import-summary.d.ts +15 -0
- package/dist/declarations/src/@types/index.d.ts +9 -0
- package/dist/declarations/src/@types/shared.d.ts +7 -0
- package/dist/declarations/src/@utils/error-mapping.d.ts +19 -0
- package/dist/declarations/src/@utils/file-upload.d.ts +46 -0
- package/dist/declarations/src/@utils/form.d.ts +1 -0
- package/dist/declarations/src/@utils/format.d.ts +5 -0
- package/dist/declarations/src/@utils/import-container.d.ts +8 -0
- package/dist/declarations/src/@utils/index.d.ts +6 -0
- package/dist/declarations/src/@utils/url.d.ts +6 -0
- package/dist/declarations/src/index.d.ts +26 -0
- package/index.js +1 -0
- package/jest.test.config.js +11 -0
- package/package.json +63 -0
- package/src/@api/export-operations.ts +26 -0
- package/src/@api/fetcher.spec.ts +51 -0
- package/src/@api/fetcher.ts +127 -0
- package/src/@api/file-upload.spec.ts +83 -0
- package/src/@api/file-upload.ts +46 -0
- package/src/@api/import-containers.ts +256 -0
- package/src/@api/import-operations.ts +33 -0
- package/src/@api/index.ts +8 -0
- package/src/@api/process-file.spec.ts +74 -0
- package/src/@api/process-file.ts +53 -0
- package/src/@api/test-fixtures.ts +772 -0
- package/src/@api/urls.ts +118 -0
- package/src/@components/file-drop-area/active-drag-drop-area.tsx +33 -0
- package/src/@components/file-drop-area/disabled-drop-area.tsx +17 -0
- package/src/@components/file-drop-area/drop-area-wrapper.tsx +38 -0
- package/src/@components/file-drop-area/enabled-drop-area.tsx +27 -0
- package/src/@components/file-drop-area/file-drop-area.tsx +74 -0
- package/src/@components/file-drop-area/file-dropped-area.tsx +29 -0
- package/src/@components/file-drop-area/index.ts +7 -0
- package/src/@components/file-drop-area/styles.ts +67 -0
- package/src/@components/icons/file-icon.tsx +30 -0
- package/src/@components/icons/index.ts +2 -0
- package/src/@components/icons/lock-icon.tsx +34 -0
- package/src/@components/index.ts +6 -0
- package/src/@components/info-box/index.ts +1 -0
- package/src/@components/info-box/info-box.tsx +23 -0
- package/src/@components/upload-separator/index.ts +1 -0
- package/src/@components/upload-separator/upload-separator.tsx +61 -0
- package/src/@components/upload-settings/index.ts +1 -0
- package/src/@components/upload-settings/upload-settings.tsx +36 -0
- package/src/@components/uploading-modal/index.ts +1 -0
- package/src/@components/uploading-modal/uploading-modal.tsx +64 -0
- package/src/@constants/delimiters.ts +14 -0
- package/src/@constants/import-tags.ts +9 -0
- package/src/@constants/index.ts +4 -0
- package/src/@constants/resource-links.ts +61 -0
- package/src/@constants/upload-limits.ts +11 -0
- package/src/@errors/http-error.ts +17 -0
- package/src/@errors/index.ts +8 -0
- package/src/@errors/invalid-response-error.ts +6 -0
- package/src/@errors/no-resources-to-export-error.ts +6 -0
- package/src/@errors/project-key-not-available-error.ts +6 -0
- package/src/@errors/query-predicate-error.ts +10 -0
- package/src/@errors/unexpected-column-error.ts +6 -0
- package/src/@errors/unexpected-operation-state-error.ts +8 -0
- package/src/@errors/unexpected-resource-type-error.ts +6 -0
- package/src/@hooks/index.ts +5 -0
- package/src/@hooks/messages.ts +11 -0
- package/src/@hooks/use-fetch-export-operations.ts +34 -0
- package/src/@hooks/use-fetch-import-container-details.ts +31 -0
- package/src/@hooks/use-fetch-import-operations.ts +42 -0
- package/src/@hooks/use-fetch-import-summaries.ts +47 -0
- package/src/@hooks/use-fetch.spec.ts +76 -0
- package/src/@hooks/use-fetch.ts +80 -0
- package/src/@hooks/use-import-container-upload.spec.ts +294 -0
- package/src/@hooks/use-import-container-upload.ts +126 -0
- package/src/@types/api.ts +14 -0
- package/src/@types/basic-error-data-type.ts +5 -0
- package/src/@types/export-operation.ts +144 -0
- package/src/@types/file-upload.ts +81 -0
- package/src/@types/import-container.ts +104 -0
- package/src/@types/import-operation.ts +31 -0
- package/src/@types/import-states.ts +9 -0
- package/src/@types/import-summary.ts +22 -0
- package/src/@types/index.ts +9 -0
- package/src/@types/shared.ts +52 -0
- package/src/@utils/error-mapping.spec.ts +126 -0
- package/src/@utils/error-mapping.ts +39 -0
- package/src/@utils/file-upload.spec.ts +151 -0
- package/src/@utils/file-upload.ts +150 -0
- package/src/@utils/form.ts +20 -0
- package/src/@utils/format.spec.ts +62 -0
- package/src/@utils/format.ts +53 -0
- package/src/@utils/import-container.spec.ts +26 -0
- package/src/@utils/import-container.ts +34 -0
- package/src/@utils/index.ts +6 -0
- package/src/@utils/url.spec.ts +75 -0
- package/src/@utils/url.ts +18 -0
- package/src/index.ts +27 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { renderHook, waitFor, act } from '@testing-library/react'
|
|
2
|
+
import {
|
|
3
|
+
createImportContainerForFileUpload,
|
|
4
|
+
deleteImportContainer,
|
|
5
|
+
uploadFileForImport,
|
|
6
|
+
} from '../@api'
|
|
7
|
+
import { ProjectKeyNotAvailableError } from '../@errors'
|
|
8
|
+
import { encodeFileNameWithTimestampToContainerKey } from '../@utils'
|
|
9
|
+
import {
|
|
10
|
+
useImportContainerUpload,
|
|
11
|
+
type UseImportContainerUploadConfig,
|
|
12
|
+
} from './use-import-container-upload'
|
|
13
|
+
|
|
14
|
+
jest.mock('../@api')
|
|
15
|
+
jest.mock('../@utils')
|
|
16
|
+
|
|
17
|
+
const mockCreateImportContainerForFileUpload =
|
|
18
|
+
createImportContainerForFileUpload as jest.MockedFunction<
|
|
19
|
+
typeof createImportContainerForFileUpload
|
|
20
|
+
>
|
|
21
|
+
const mockUploadFileForImport = uploadFileForImport as jest.MockedFunction<
|
|
22
|
+
typeof uploadFileForImport
|
|
23
|
+
>
|
|
24
|
+
const mockDeleteImportContainer = deleteImportContainer as jest.MockedFunction<
|
|
25
|
+
typeof deleteImportContainer
|
|
26
|
+
>
|
|
27
|
+
const mockEncodeFileNameWithTimestampToContainerKey =
|
|
28
|
+
encodeFileNameWithTimestampToContainerKey as jest.MockedFunction<
|
|
29
|
+
typeof encodeFileNameWithTimestampToContainerKey
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
describe('useImportContainerUpload', () => {
|
|
33
|
+
const projectKey = 'test-with-big-data'
|
|
34
|
+
const importContainerKey = 'test-container-key'
|
|
35
|
+
const mockFile = new File(['test content'], 'test.csv', { type: 'text/csv' })
|
|
36
|
+
const mockXhr = { abort: jest.fn() } as unknown as XMLHttpRequest
|
|
37
|
+
const mockFileUploadResponse = {
|
|
38
|
+
results: [],
|
|
39
|
+
invalid: 0,
|
|
40
|
+
valid: 10,
|
|
41
|
+
fileName: 'test.csv',
|
|
42
|
+
itemsCount: 10,
|
|
43
|
+
rowsCount: 10,
|
|
44
|
+
columnsCount: 2,
|
|
45
|
+
columns: ['key', 'value'],
|
|
46
|
+
ignoredColumns: [],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.clearAllMocks()
|
|
51
|
+
mockEncodeFileNameWithTimestampToContainerKey.mockReturnValue(
|
|
52
|
+
importContainerKey
|
|
53
|
+
)
|
|
54
|
+
mockCreateImportContainerForFileUpload.mockResolvedValue(undefined)
|
|
55
|
+
mockDeleteImportContainer.mockResolvedValue(undefined)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should throw "ProjectKeyNotAvailableError" if "projectKey" is not available', async () => {
|
|
59
|
+
const { result } = renderHook(() =>
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
useImportContainerUpload({ projectKey: undefined })
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await expect(
|
|
65
|
+
result.current.upload({
|
|
66
|
+
file: mockFile,
|
|
67
|
+
resourceType: 'product',
|
|
68
|
+
onSuccess: jest.fn(),
|
|
69
|
+
})
|
|
70
|
+
).rejects.toThrow(ProjectKeyNotAvailableError)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should create container and upload file successfully', async () => {
|
|
74
|
+
const onSuccess = jest.fn()
|
|
75
|
+
const onProgress = jest.fn()
|
|
76
|
+
|
|
77
|
+
mockUploadFileForImport.mockImplementation(({ onSuccess: _onSuccess }) => {
|
|
78
|
+
_onSuccess(mockFileUploadResponse)
|
|
79
|
+
return mockXhr
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const { result } = renderHook(() =>
|
|
83
|
+
useImportContainerUpload({ projectKey })
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const uploadConfig: UseImportContainerUploadConfig = {
|
|
87
|
+
file: mockFile,
|
|
88
|
+
resourceType: 'product',
|
|
89
|
+
settings: { format: 'CSV' },
|
|
90
|
+
onSuccess,
|
|
91
|
+
onProgress,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await act(async () => {
|
|
95
|
+
await result.current.upload(uploadConfig)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(mockCreateImportContainerForFileUpload).toHaveBeenCalledWith({
|
|
99
|
+
importContainerDraft: {
|
|
100
|
+
key: importContainerKey,
|
|
101
|
+
resourceType: 'product',
|
|
102
|
+
tags: ['source:file-upload'],
|
|
103
|
+
settings: { format: 'CSV' },
|
|
104
|
+
},
|
|
105
|
+
projectKey,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(mockUploadFileForImport).toHaveBeenCalledWith({
|
|
109
|
+
projectKey,
|
|
110
|
+
importContainerKey,
|
|
111
|
+
resourceType: 'product',
|
|
112
|
+
file: mockFile,
|
|
113
|
+
onSuccess: expect.any(Function),
|
|
114
|
+
onProgress: expect.any(Function),
|
|
115
|
+
onError: expect.any(Function),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(onSuccess).toHaveBeenCalledWith(
|
|
119
|
+
mockFileUploadResponse,
|
|
120
|
+
importContainerKey
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should delete container on upload error', async () => {
|
|
125
|
+
const onError = jest.fn()
|
|
126
|
+
const uploadError = new Error('Upload failed')
|
|
127
|
+
|
|
128
|
+
mockUploadFileForImport.mockImplementation(({ onError: _onError }) => {
|
|
129
|
+
_onError?.(uploadError)
|
|
130
|
+
return mockXhr
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const { result } = renderHook(() =>
|
|
134
|
+
useImportContainerUpload({ projectKey })
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
await result.current.upload({
|
|
139
|
+
file: mockFile,
|
|
140
|
+
resourceType: 'product',
|
|
141
|
+
onSuccess: jest.fn(),
|
|
142
|
+
onError,
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(mockDeleteImportContainer).toHaveBeenCalledWith({
|
|
148
|
+
projectKey,
|
|
149
|
+
importContainerKey,
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
expect(onError).toHaveBeenCalledWith(uploadError)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should delete container if upload throws before starting', async () => {
|
|
156
|
+
const error = new Error('Container creation failed')
|
|
157
|
+
mockCreateImportContainerForFileUpload.mockRejectedValue(error)
|
|
158
|
+
|
|
159
|
+
const { result } = renderHook(() =>
|
|
160
|
+
useImportContainerUpload({ projectKey })
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
await expect(
|
|
164
|
+
act(async () => {
|
|
165
|
+
await result.current.upload({
|
|
166
|
+
file: mockFile,
|
|
167
|
+
resourceType: 'product',
|
|
168
|
+
onSuccess: jest.fn(),
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
).rejects.toThrow(error)
|
|
172
|
+
|
|
173
|
+
expect(mockDeleteImportContainer).toHaveBeenCalledWith({
|
|
174
|
+
projectKey,
|
|
175
|
+
importContainerKey,
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should update progress during upload', async () => {
|
|
180
|
+
const onProgress = jest.fn()
|
|
181
|
+
|
|
182
|
+
mockUploadFileForImport.mockImplementation(
|
|
183
|
+
({ onProgress: _onProgress }) => {
|
|
184
|
+
_onProgress?.(50)
|
|
185
|
+
_onProgress?.(100)
|
|
186
|
+
return mockXhr
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const { result } = renderHook(() =>
|
|
191
|
+
useImportContainerUpload({ projectKey })
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
await act(async () => {
|
|
195
|
+
await result.current.upload({
|
|
196
|
+
file: mockFile,
|
|
197
|
+
resourceType: 'product',
|
|
198
|
+
onSuccess: jest.fn(),
|
|
199
|
+
onProgress,
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(onProgress).toHaveBeenCalledWith(50)
|
|
204
|
+
expect(onProgress).toHaveBeenCalledWith(100)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should update isUploading state correctly', async () => {
|
|
208
|
+
mockUploadFileForImport.mockImplementation(({ onSuccess }) => {
|
|
209
|
+
setTimeout(() => onSuccess(mockFileUploadResponse), 100)
|
|
210
|
+
return mockXhr
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const { result } = renderHook(() =>
|
|
214
|
+
useImportContainerUpload({ projectKey })
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
expect(result.current.isUploading).toBe(false)
|
|
218
|
+
|
|
219
|
+
act(() => {
|
|
220
|
+
result.current.upload({
|
|
221
|
+
file: mockFile,
|
|
222
|
+
resourceType: 'product',
|
|
223
|
+
onSuccess: jest.fn(),
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
expect(result.current.isUploading).toBe(true)
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(result.current.isUploading).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should abort upload when abort is called', async () => {
|
|
235
|
+
mockUploadFileForImport.mockReturnValue(mockXhr)
|
|
236
|
+
|
|
237
|
+
const { result } = renderHook(() =>
|
|
238
|
+
useImportContainerUpload({ projectKey })
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.upload({
|
|
243
|
+
file: mockFile,
|
|
244
|
+
resourceType: 'product',
|
|
245
|
+
onSuccess: jest.fn(),
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(mockUploadFileForImport).toHaveBeenCalled()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
result.current.abort()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(mockXhr.abort).toHaveBeenCalled()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should work without optional settings', async () => {
|
|
261
|
+
const onSuccess = jest.fn()
|
|
262
|
+
|
|
263
|
+
mockUploadFileForImport.mockImplementation(({ onSuccess: _onSuccess }) => {
|
|
264
|
+
_onSuccess(mockFileUploadResponse)
|
|
265
|
+
return mockXhr
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const { result } = renderHook(() =>
|
|
269
|
+
useImportContainerUpload({ projectKey })
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
await act(async () => {
|
|
273
|
+
await result.current.upload({
|
|
274
|
+
file: mockFile,
|
|
275
|
+
resourceType: 'product',
|
|
276
|
+
onSuccess,
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
expect(mockCreateImportContainerForFileUpload).toHaveBeenCalledWith({
|
|
281
|
+
importContainerDraft: {
|
|
282
|
+
key: importContainerKey,
|
|
283
|
+
resourceType: 'product',
|
|
284
|
+
tags: ['source:file-upload'],
|
|
285
|
+
},
|
|
286
|
+
projectKey,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
expect(onSuccess).toHaveBeenCalledWith(
|
|
290
|
+
mockFileUploadResponse,
|
|
291
|
+
importContainerKey
|
|
292
|
+
)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
3
|
+
import {
|
|
4
|
+
createImportContainerForFileUpload,
|
|
5
|
+
deleteImportContainer,
|
|
6
|
+
uploadFileForImport,
|
|
7
|
+
} from '../@api'
|
|
8
|
+
import { ProjectKeyNotAvailableError } from '../@errors'
|
|
9
|
+
import { TAG_KEY_SOURCE_FILE_UPLOAD } from '../@constants'
|
|
10
|
+
import { encodeFileNameWithTimestampToContainerKey } from '../@utils'
|
|
11
|
+
import type {
|
|
12
|
+
FileUploadResponse,
|
|
13
|
+
ExtendedImportContainerDraft,
|
|
14
|
+
} from '../@types'
|
|
15
|
+
|
|
16
|
+
export type UseImportContainerUploadConfig = {
|
|
17
|
+
file: File
|
|
18
|
+
resourceType: ResourceTypeId
|
|
19
|
+
settings?: ExtendedImportContainerDraft['settings']
|
|
20
|
+
onSuccess: (response: FileUploadResponse, importContainerKey: string) => void
|
|
21
|
+
onError?: (error: unknown) => void
|
|
22
|
+
onProgress?: (progress: number) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useImportContainerUpload = ({
|
|
26
|
+
projectKey,
|
|
27
|
+
}: {
|
|
28
|
+
projectKey: string
|
|
29
|
+
}) => {
|
|
30
|
+
const [isUploading, setIsUploading] = React.useState(false)
|
|
31
|
+
const [progress, setProgress] = React.useState(0)
|
|
32
|
+
const xhrRef = React.useRef<XMLHttpRequest | null>(null)
|
|
33
|
+
|
|
34
|
+
const upload = async ({
|
|
35
|
+
file,
|
|
36
|
+
resourceType,
|
|
37
|
+
settings,
|
|
38
|
+
onSuccess,
|
|
39
|
+
onError,
|
|
40
|
+
onProgress,
|
|
41
|
+
}: UseImportContainerUploadConfig): Promise<XMLHttpRequest> => {
|
|
42
|
+
if (!projectKey) {
|
|
43
|
+
throw new ProjectKeyNotAvailableError()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsUploading(true)
|
|
47
|
+
setProgress(0)
|
|
48
|
+
|
|
49
|
+
const importContainerKey = encodeFileNameWithTimestampToContainerKey(
|
|
50
|
+
file.name
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await createImportContainerForFileUpload({
|
|
55
|
+
importContainerDraft: {
|
|
56
|
+
key: importContainerKey,
|
|
57
|
+
resourceType,
|
|
58
|
+
tags: [TAG_KEY_SOURCE_FILE_UPLOAD],
|
|
59
|
+
...(settings ? { settings } : {}),
|
|
60
|
+
},
|
|
61
|
+
projectKey,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const xhr = uploadFileForImport({
|
|
65
|
+
projectKey,
|
|
66
|
+
importContainerKey,
|
|
67
|
+
resourceType,
|
|
68
|
+
file,
|
|
69
|
+
onSuccess: (response) => {
|
|
70
|
+
setIsUploading(false)
|
|
71
|
+
setProgress(100)
|
|
72
|
+
onSuccess(response, importContainerKey)
|
|
73
|
+
},
|
|
74
|
+
onProgress: (prog) => {
|
|
75
|
+
setProgress(prog)
|
|
76
|
+
onProgress?.(prog)
|
|
77
|
+
},
|
|
78
|
+
onError: async (error) => {
|
|
79
|
+
try {
|
|
80
|
+
await deleteImportContainer({
|
|
81
|
+
projectKey,
|
|
82
|
+
importContainerKey,
|
|
83
|
+
})
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore cleanup errors - container will be cleaned up by TTL retention policy
|
|
86
|
+
// Cleanup errors are unlikely unless there is a network issue or container was removed externally
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setIsUploading(false)
|
|
90
|
+
setProgress(0)
|
|
91
|
+
onError?.(error)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
xhrRef.current = xhr
|
|
96
|
+
return xhr
|
|
97
|
+
} catch (error) {
|
|
98
|
+
try {
|
|
99
|
+
await deleteImportContainer({
|
|
100
|
+
projectKey,
|
|
101
|
+
importContainerKey,
|
|
102
|
+
})
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore cleanup errors - container will be cleaned up by TTL retention policy
|
|
105
|
+
// Cleanup errors are unlikely unless there is a network issue or container was removed externally
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setIsUploading(false)
|
|
109
|
+
setProgress(0)
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const abort = () => {
|
|
115
|
+
xhrRef.current?.abort()
|
|
116
|
+
setIsUploading(false)
|
|
117
|
+
setProgress(0)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
upload,
|
|
122
|
+
abort,
|
|
123
|
+
isUploading,
|
|
124
|
+
progress,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type METHOD = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
2
|
+
|
|
3
|
+
export type ApiConfig = {
|
|
4
|
+
headers?: Record<string, string | null>
|
|
5
|
+
method?: METHOD
|
|
6
|
+
proxy?: string
|
|
7
|
+
abortSignal?: AbortSignal
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Fetcher = {
|
|
11
|
+
url: string
|
|
12
|
+
payload?: BodyInit
|
|
13
|
+
config?: ApiConfig
|
|
14
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
2
|
+
import { hasRequiredFields } from './shared'
|
|
3
|
+
|
|
4
|
+
export type ExportOperationState =
|
|
5
|
+
| 'processing'
|
|
6
|
+
| 'completed'
|
|
7
|
+
| 'failed'
|
|
8
|
+
| 'queued'
|
|
9
|
+
|
|
10
|
+
export type ResourceType = ResourceTypeId
|
|
11
|
+
|
|
12
|
+
export type FileFormat = 'json' | 'csv'
|
|
13
|
+
|
|
14
|
+
export type Separator = {
|
|
15
|
+
columns?: string
|
|
16
|
+
arrayValues?: string
|
|
17
|
+
decimal?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ExportOperation = {
|
|
21
|
+
id: string
|
|
22
|
+
fileName: string
|
|
23
|
+
createdAt: string
|
|
24
|
+
resourceType: ResourceType
|
|
25
|
+
state: ExportOperationState
|
|
26
|
+
completedPercentage: number
|
|
27
|
+
resourceCount: number
|
|
28
|
+
fileFormat: FileFormat
|
|
29
|
+
fields: string[]
|
|
30
|
+
locales: string[]
|
|
31
|
+
query?: string
|
|
32
|
+
filters?: {
|
|
33
|
+
filters: string[]
|
|
34
|
+
fullText?: {
|
|
35
|
+
text: string
|
|
36
|
+
locale: string
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fillRows?: boolean
|
|
40
|
+
excelCompatible?: boolean
|
|
41
|
+
dryRun?: boolean
|
|
42
|
+
separator?: Separator
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PaginatedExportOperationResponse {
|
|
46
|
+
results: ExportOperation[]
|
|
47
|
+
limit: number
|
|
48
|
+
offset: number
|
|
49
|
+
total: number
|
|
50
|
+
count: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type DownloadFile = {
|
|
54
|
+
uri: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ExportOperationQueryParams = {
|
|
58
|
+
limit: number
|
|
59
|
+
offset: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertPaginatedExportOperationResponse(
|
|
63
|
+
maybePaginatedExportOperationResponseOrExportOperationsResponse: unknown
|
|
64
|
+
): asserts maybePaginatedExportOperationResponseOrExportOperationsResponse is PaginatedExportOperationResponse {
|
|
65
|
+
const requiredFields = ['results', 'total']
|
|
66
|
+
if (
|
|
67
|
+
hasRequiredFields(
|
|
68
|
+
maybePaginatedExportOperationResponseOrExportOperationsResponse,
|
|
69
|
+
requiredFields
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
throw new Error('Invalid response')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function assertExportOperationsDownloadFileResponse(
|
|
77
|
+
maybeExportOperationDownloadFileResponse: unknown
|
|
78
|
+
): asserts maybeExportOperationDownloadFileResponse is DownloadFile {
|
|
79
|
+
const requiredFields = ['uri']
|
|
80
|
+
if (
|
|
81
|
+
hasRequiredFields(maybeExportOperationDownloadFileResponse, requiredFields)
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
throw new Error('Invalid response')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type QueryFilter = {
|
|
88
|
+
query?: Record<string, unknown>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type SearchFilter = {
|
|
92
|
+
filters?: string[]
|
|
93
|
+
fullText?: {
|
|
94
|
+
text: string
|
|
95
|
+
locale: string
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ExportApiRequest {
|
|
100
|
+
projectKey: string
|
|
101
|
+
resourceType: string
|
|
102
|
+
fileName: string
|
|
103
|
+
fileFormat: string
|
|
104
|
+
fields: string[]
|
|
105
|
+
locales?: string[]
|
|
106
|
+
filters?: QueryFilter | SearchFilter
|
|
107
|
+
where?: string
|
|
108
|
+
fullText?: {
|
|
109
|
+
text: string
|
|
110
|
+
locale: string
|
|
111
|
+
}
|
|
112
|
+
fillRows?: boolean
|
|
113
|
+
excelCompatible?: boolean
|
|
114
|
+
separator?: Separator
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ExportApiSuccessResponse {
|
|
118
|
+
id: string
|
|
119
|
+
state: string
|
|
120
|
+
resourceType: string
|
|
121
|
+
resourceCount: number
|
|
122
|
+
createdAt: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ExportApiErrorResponse {
|
|
126
|
+
statusCode: number
|
|
127
|
+
error: string
|
|
128
|
+
errors: ValidationError[]
|
|
129
|
+
message: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type ValidationErrorCode =
|
|
133
|
+
| 'DuplicateFields'
|
|
134
|
+
| 'NotSupportedField'
|
|
135
|
+
| 'FieldDefinitionNotFound'
|
|
136
|
+
| 'AttributeDefinitionNotFound'
|
|
137
|
+
| 'LocalizedFieldWithoutLocale'
|
|
138
|
+
| 'IncompleteField'
|
|
139
|
+
|
|
140
|
+
export type ValidationError = {
|
|
141
|
+
code: ValidationErrorCode
|
|
142
|
+
message: string
|
|
143
|
+
field?: string
|
|
144
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
2
|
+
|
|
3
|
+
export type FileUploadError = {
|
|
4
|
+
title: string
|
|
5
|
+
description: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type UploadFileErrorRow = {
|
|
9
|
+
id: string
|
|
10
|
+
row?: number
|
|
11
|
+
index?: number
|
|
12
|
+
code: string
|
|
13
|
+
field: string
|
|
14
|
+
validationMessage: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RowError = {
|
|
18
|
+
code: string
|
|
19
|
+
message: string
|
|
20
|
+
field: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RowErrorsResponse = {
|
|
24
|
+
row?: number
|
|
25
|
+
index?: number
|
|
26
|
+
errors: Array<RowError>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FileUploadResponse = {
|
|
30
|
+
results: Array<RowErrorsResponse>
|
|
31
|
+
invalid: number
|
|
32
|
+
valid: number
|
|
33
|
+
fileName: string
|
|
34
|
+
itemsCount: number
|
|
35
|
+
rowsCount: number
|
|
36
|
+
columnsCount: number
|
|
37
|
+
columns: Array<string>
|
|
38
|
+
ignoredColumns: Array<string>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FileUploadRequestParameters {
|
|
42
|
+
projectKey: string
|
|
43
|
+
importContainerKey: string
|
|
44
|
+
resourceType: ResourceTypeId
|
|
45
|
+
file: File
|
|
46
|
+
abortSignal?: AbortSignal
|
|
47
|
+
onSuccess: (data: FileUploadResponse) => void
|
|
48
|
+
onProgress: (progress: number) => void
|
|
49
|
+
onError: (error: Error) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ProcessUploadedFileRequestParameters {
|
|
53
|
+
projectKey: string
|
|
54
|
+
importContainerKey: string
|
|
55
|
+
resourceType: ResourceTypeId
|
|
56
|
+
action?: 'delete' // Currently only 'delete' action is supported
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ProcessFileResponse = {
|
|
60
|
+
message: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type MissingCsvFieldIdentifierError = {
|
|
64
|
+
code: string
|
|
65
|
+
message: string
|
|
66
|
+
rowValue: Record<string, unknown>
|
|
67
|
+
metadata: {
|
|
68
|
+
row: number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type DecimalSeparator = '.' | ','
|
|
73
|
+
|
|
74
|
+
export type DropAreaState =
|
|
75
|
+
| 'default'
|
|
76
|
+
| 'active-drag'
|
|
77
|
+
| 'ready-for-drop'
|
|
78
|
+
| 'file-dropped'
|
|
79
|
+
| 'disabled'
|
|
80
|
+
| 'invalid'
|
|
81
|
+
| 'is-parsing'
|