@commercetools-frontend-extensions/operations 2.0.1 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +81 -72
  3. package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +947 -143
  4. package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +947 -143
  5. package/dist/commercetools-frontend-extensions-operations.esm.js +898 -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 +3 -0
  17. package/dist/declarations/src/@hooks/use-fetch-file-import-job.d.ts +17 -0
  18. package/dist/declarations/src/@hooks/use-file-import-job-upload.d.ts +18 -0
  19. package/dist/declarations/src/@hooks/use-file-upload.d.ts +28 -0
  20. package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +2 -1
  21. package/dist/declarations/src/@types/export-operation.d.ts +3 -1
  22. package/dist/declarations/src/@types/file-import-job.d.ts +99 -0
  23. package/dist/declarations/src/@types/file-upload-result.d.ts +21 -0
  24. package/dist/declarations/src/@types/file-upload.d.ts +2 -2
  25. package/dist/declarations/src/@types/index.d.ts +2 -0
  26. package/dist/declarations/src/@utils/file-import-job-helpers.d.ts +12 -0
  27. package/dist/declarations/src/@utils/file-upload.d.ts +8 -0
  28. package/dist/declarations/src/@utils/index.d.ts +2 -0
  29. package/dist/declarations/src/@utils/poll-job-until-validated.d.ts +11 -0
  30. package/package.json +12 -13
  31. package/src/@api/fetcher.ts +10 -0
  32. package/src/@api/file-import-jobs.ts +217 -0
  33. package/src/@api/file-upload.spec.ts +4 -2
  34. package/src/@api/index.ts +1 -0
  35. package/src/@api/test-fixtures.ts +127 -5
  36. package/src/@api/urls.ts +77 -1
  37. package/src/@components/uploading-modal/uploading-modal.tsx +7 -5
  38. package/src/@constants/file-import-job.ts +1 -0
  39. package/src/@constants/import-limits.ts +13 -0
  40. package/src/@constants/index.ts +2 -1
  41. package/src/@errors/index.ts +5 -4
  42. package/src/@errors/polling-aborted-error.ts +6 -0
  43. package/src/@hooks/index.ts +3 -0
  44. package/src/@hooks/use-fetch-file-import-job.spec.ts +131 -0
  45. package/src/@hooks/use-fetch-file-import-job.ts +38 -0
  46. package/src/@hooks/use-fetch.spec.ts +1 -9
  47. package/src/@hooks/use-fetch.ts +4 -8
  48. package/src/@hooks/use-file-import-job-upload.spec.ts +273 -0
  49. package/src/@hooks/use-file-import-job-upload.ts +101 -0
  50. package/src/@hooks/use-file-upload.ts +223 -0
  51. package/src/@hooks/use-import-container-upload.spec.ts +16 -13
  52. package/src/@hooks/use-import-container-upload.ts +6 -2
  53. package/src/@types/export-operation.ts +3 -0
  54. package/src/@types/file-import-job.ts +165 -0
  55. package/src/@types/file-upload-result.ts +23 -0
  56. package/src/@types/file-upload.ts +2 -2
  57. package/src/@types/index.ts +2 -0
  58. package/src/@utils/error-mapping.ts +10 -9
  59. package/src/@utils/file-import-job-helpers.spec.ts +147 -0
  60. package/src/@utils/file-import-job-helpers.ts +47 -0
  61. package/src/@utils/file-upload.ts +39 -0
  62. package/src/@utils/index.ts +2 -0
  63. package/src/@utils/poll-job-until-validated.ts +76 -0
  64. package/dist/declarations/src/@constants/upload-limits.d.ts +0 -10
  65. package/src/@constants/upload-limits.ts +0 -11
  66. package/src/@hooks/messages.ts +0 -11
@@ -0,0 +1,131 @@
1
+ import { renderHook, waitFor } from '@testing-library/react'
2
+ import { getFileImportJob } from '../@api'
3
+ import { useFetchFileImportJob } from './use-fetch-file-import-job'
4
+ import type { FileImportJob } from '../@types'
5
+
6
+ jest.mock('@commercetools-frontend/sentry', () => ({
7
+ reportErrorToSentry: jest.fn(),
8
+ }))
9
+ jest.mock('../@api', () => ({
10
+ getFileImportJob: jest.fn(),
11
+ }))
12
+
13
+ const mockGetFileImportJob = getFileImportJob as jest.MockedFunction<
14
+ typeof getFileImportJob
15
+ >
16
+
17
+ describe('useFetchFileImportJob', () => {
18
+ const projectKey = 'test-with-big-data'
19
+ const resourceType = 'product'
20
+ const importContainerKey = 'test-container'
21
+ const jobId = 'test-job-id'
22
+
23
+ const mockJob: FileImportJob = {
24
+ id: jobId,
25
+ fileName: 'test.csv',
26
+ importContainerKey,
27
+ state: 'validated',
28
+ summary: {
29
+ total: 100,
30
+ invalid: 5,
31
+ valid: 95,
32
+ fieldsCount: 10,
33
+ fields: ['field1', 'field2'],
34
+ ignoredFields: [],
35
+ },
36
+ }
37
+
38
+ beforeEach(() => {
39
+ jest.clearAllMocks()
40
+ })
41
+
42
+ it('should fetch job details', async () => {
43
+ mockGetFileImportJob.mockResolvedValue(mockJob)
44
+
45
+ const { result } = renderHook(() =>
46
+ useFetchFileImportJob({
47
+ projectKey,
48
+ resourceType,
49
+ importContainerKey,
50
+ jobId,
51
+ })
52
+ )
53
+
54
+ await waitFor(() => {
55
+ expect(result.current.data).toEqual(mockJob)
56
+ })
57
+
58
+ expect(mockGetFileImportJob).toHaveBeenCalledWith({
59
+ projectKey,
60
+ resourceType,
61
+ importContainerKey,
62
+ jobId,
63
+ })
64
+ })
65
+
66
+ it('should handle errors', async () => {
67
+ const error = new Error('Failed to fetch job')
68
+ mockGetFileImportJob.mockRejectedValue(error)
69
+
70
+ const { result } = renderHook(() =>
71
+ useFetchFileImportJob({
72
+ projectKey,
73
+ resourceType,
74
+ importContainerKey,
75
+ jobId,
76
+ })
77
+ )
78
+
79
+ await waitFor(() => {
80
+ expect(result.current.error).toBeTruthy()
81
+ })
82
+ })
83
+
84
+ it('should pass polling config to useFetch', async () => {
85
+ mockGetFileImportJob.mockResolvedValue(mockJob)
86
+
87
+ const pollingInterval = 100
88
+ const shouldContinuePolling = jest.fn(
89
+ (data: typeof mockJob) => data.state !== 'validated'
90
+ )
91
+
92
+ const { result } = renderHook(() =>
93
+ useFetchFileImportJob({
94
+ projectKey,
95
+ resourceType,
96
+ importContainerKey,
97
+ jobId,
98
+ pollingInterval,
99
+ shouldContinuePolling,
100
+ })
101
+ )
102
+
103
+ await waitFor(() => {
104
+ expect(result.current.data).toEqual(mockJob)
105
+ })
106
+
107
+ expect(mockGetFileImportJob).toHaveBeenCalledWith({
108
+ projectKey,
109
+ resourceType,
110
+ importContainerKey,
111
+ jobId,
112
+ })
113
+ })
114
+
115
+ it('should throw ProjectKeyNotAvailableError when projectKey is empty', async () => {
116
+ const { result } = renderHook(() =>
117
+ useFetchFileImportJob({
118
+ projectKey: '',
119
+ resourceType,
120
+ importContainerKey,
121
+ jobId,
122
+ })
123
+ )
124
+
125
+ await waitFor(() => {
126
+ expect(result.current.error).toBeTruthy()
127
+ })
128
+
129
+ expect(result.current.error?.message).toBe('Project key is not available')
130
+ })
131
+ })
@@ -0,0 +1,38 @@
1
+ import React from 'react'
2
+ import { getFileImportJob } from '../@api'
3
+ import { ProjectKeyNotAvailableError } from '../@errors'
4
+ import type { FileImportJob } from '../@types'
5
+ import { useFetch } from './use-fetch'
6
+
7
+ type UseFetchFileImportJobConfig = {
8
+ projectKey: string
9
+ resourceType: string
10
+ importContainerKey: string
11
+ jobId: string
12
+ pollingInterval?: number
13
+ shouldContinuePolling?: (data: FileImportJob) => boolean
14
+ }
15
+
16
+ export const useFetchFileImportJob = ({
17
+ projectKey,
18
+ importContainerKey,
19
+ jobId,
20
+ pollingInterval,
21
+ shouldContinuePolling,
22
+ }: UseFetchFileImportJobConfig) => {
23
+ const fetchData = React.useCallback(() => {
24
+ if (!projectKey) {
25
+ return Promise.reject(new ProjectKeyNotAvailableError())
26
+ }
27
+ return getFileImportJob({
28
+ projectKey,
29
+ importContainerKey,
30
+ jobId,
31
+ })
32
+ }, [projectKey, importContainerKey, jobId])
33
+
34
+ return useFetch(fetchData, {
35
+ pollingInterval,
36
+ shouldContinuePolling,
37
+ })
38
+ }
@@ -1,23 +1,15 @@
1
1
  import { renderHook, act, waitFor } from '@testing-library/react'
2
- import { useIntl } from 'react-intl'
3
2
  import { HttpError } from '../@errors'
4
3
  import { useFetch } from './use-fetch'
5
4
 
6
- jest.mock('react-intl', () => ({
7
- useIntl: jest.fn(),
8
- defineMessages: jest.fn(),
9
- }))
10
5
  jest.mock('@commercetools-frontend/sentry', () => ({
11
6
  reportErrorToSentry: jest.fn(),
12
7
  }))
13
8
 
14
9
  describe('useFetch', () => {
15
- const mockIntl = {
16
- formatMessage: jest.fn((message) => message.defaultMessage),
17
- }
18
10
  const mockFetchFunction = jest.fn()
11
+
19
12
  beforeEach(() => {
20
- ;(useIntl as jest.Mock).mockReturnValue(mockIntl)
21
13
  jest.clearAllMocks()
22
14
  })
23
15
  it('should fetch data successfully', async () => {
@@ -1,8 +1,6 @@
1
1
  import React from 'react'
2
- import { useIntl } from 'react-intl'
3
2
  import { reportErrorToSentry } from '@commercetools-frontend/sentry'
4
3
  import { HttpError } from '../@errors'
5
- import messages from './messages'
6
4
 
7
5
  export const useFetch = <Data>(
8
6
  fetchFunction: () => Promise<Data>,
@@ -11,7 +9,6 @@ export const useFetch = <Data>(
11
9
  shouldContinuePolling?: (data: Data) => boolean
12
10
  } = {}
13
11
  ) => {
14
- const intl = useIntl()
15
12
  const [data, setData] = React.useState<Data | null>(null)
16
13
  const [error, setError] = React.useState<Error | null>(null)
17
14
  const [isLoading, setIsLoading] = React.useState(false)
@@ -40,10 +37,10 @@ export const useFetch = <Data>(
40
37
  }
41
38
  }
42
39
  } catch (err) {
43
- if (err instanceof HttpError) {
44
- setError(err)
45
- } else {
46
- setError(new Error(intl.formatMessage(messages.unexpectedError)))
40
+ const error = err instanceof Error ? err : new Error(String(err))
41
+ setError(error)
42
+
43
+ if (!(err instanceof HttpError)) {
47
44
  reportErrorToSentry(
48
45
  new Error('An unexpected error occurred in the `useFetch` hook'),
49
46
  {
@@ -71,7 +68,6 @@ export const useFetch = <Data>(
71
68
  }, [
72
69
  fetchFunction,
73
70
  refetchCount,
74
- intl,
75
71
  config.pollingInterval,
76
72
  config.shouldContinuePolling,
77
73
  ])
@@ -0,0 +1,273 @@
1
+ import { renderHook, waitFor, act } from '@testing-library/react'
2
+ import {
3
+ createImportContainerForFileUpload,
4
+ deleteImportContainer,
5
+ createFileImportJob,
6
+ } from '../@api'
7
+ import { ProjectKeyNotAvailableError } from '../@errors'
8
+ import { encodeFileNameWithTimestampToContainerKey } from '../@utils'
9
+ import {
10
+ useFileImportJobUpload,
11
+ UseFileImportJobUploadConfig,
12
+ } from './use-file-import-job-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 mockCreateFileImportJob = createFileImportJob as jest.MockedFunction<
22
+ typeof createFileImportJob
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('useFileImportJobUpload', () => {
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 mockJobResponse = {
37
+ id: 'job-123',
38
+ fileName: 'test.csv',
39
+ importContainerKey,
40
+ state: 'queued' as const,
41
+ summary: {
42
+ total: 0,
43
+ invalid: 0,
44
+ valid: 0,
45
+ fieldsCount: 0,
46
+ fields: [],
47
+ ignoredFields: [],
48
+ },
49
+ }
50
+
51
+ beforeEach(() => {
52
+ jest.clearAllMocks()
53
+ mockEncodeFileNameWithTimestampToContainerKey.mockReturnValue(
54
+ importContainerKey
55
+ )
56
+ mockCreateImportContainerForFileUpload.mockResolvedValue({
57
+ key: importContainerKey,
58
+ })
59
+ mockCreateFileImportJob.mockResolvedValue(mockJobResponse)
60
+ mockDeleteImportContainer.mockResolvedValue(undefined)
61
+ })
62
+
63
+ it('should throw "ProjectKeyNotAvailableError" if "projectKey" is not available', async () => {
64
+ const { result } = renderHook(() =>
65
+ // @ts-ignore
66
+ useFileImportJobUpload({ projectKey: undefined })
67
+ )
68
+
69
+ await expect(
70
+ result.current.upload({
71
+ file: mockFile,
72
+ resourceType: 'product',
73
+ onSuccess: jest.fn(),
74
+ })
75
+ ).rejects.toThrow(ProjectKeyNotAvailableError)
76
+ })
77
+
78
+ it('should create container and file import job successfully', async () => {
79
+ const onSuccess = jest.fn()
80
+ const onProgress = jest.fn()
81
+
82
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
83
+
84
+ const uploadConfig: UseFileImportJobUploadConfig = {
85
+ file: mockFile,
86
+ resourceType: 'product',
87
+ settings: { format: 'CSV' },
88
+ onSuccess,
89
+ onProgress,
90
+ }
91
+
92
+ await act(async () => {
93
+ await result.current.upload(uploadConfig)
94
+ })
95
+
96
+ expect(mockCreateImportContainerForFileUpload).toHaveBeenCalledWith({
97
+ importContainerDraft: {
98
+ key: importContainerKey,
99
+ resourceType: 'product',
100
+ tags: ['source:file-upload'],
101
+ settings: { format: 'CSV' },
102
+ },
103
+ projectKey,
104
+ })
105
+
106
+ expect(mockCreateFileImportJob).toHaveBeenCalledWith({
107
+ projectKey,
108
+ resourceType: 'product',
109
+ importContainerKey,
110
+ payload: {
111
+ fileType: 'csv',
112
+ fileName: 'test.csv',
113
+ file: mockFile,
114
+ },
115
+ onProgress: expect.any(Function),
116
+ })
117
+
118
+ expect(onSuccess).toHaveBeenCalledWith('job-123', importContainerKey)
119
+ })
120
+
121
+ it('should use json fileType for custom-object resource type', async () => {
122
+ const onSuccess = jest.fn()
123
+ const mockJsonFile = new File(
124
+ ['[{"key": "test-key", "value": {"number": "1.5"}}]'],
125
+ 'test.json',
126
+ { type: 'application/json' }
127
+ )
128
+
129
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
130
+
131
+ const uploadConfig: UseFileImportJobUploadConfig = {
132
+ file: mockJsonFile,
133
+ resourceType: 'custom-object',
134
+ onSuccess,
135
+ }
136
+
137
+ await act(async () => {
138
+ await result.current.upload(uploadConfig)
139
+ })
140
+
141
+ expect(mockCreateFileImportJob).toHaveBeenCalledWith({
142
+ projectKey,
143
+ resourceType: 'custom-object',
144
+ importContainerKey,
145
+ payload: {
146
+ fileType: 'json',
147
+ fileName: 'test.json',
148
+ file: mockJsonFile,
149
+ },
150
+ onProgress: expect.any(Function),
151
+ })
152
+
153
+ expect(onSuccess).toHaveBeenCalledWith('job-123', importContainerKey)
154
+ })
155
+
156
+ it('should delete container on job creation error', async () => {
157
+ const onError = jest.fn()
158
+ const jobError = new Error('Job creation failed')
159
+ mockCreateFileImportJob.mockRejectedValue(jobError)
160
+
161
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
162
+
163
+ await act(async () => {
164
+ await result.current.upload({
165
+ file: mockFile,
166
+ resourceType: 'product',
167
+ onSuccess: jest.fn(),
168
+ onError,
169
+ })
170
+ })
171
+
172
+ await waitFor(() => {
173
+ expect(mockDeleteImportContainer).toHaveBeenCalledWith({
174
+ projectKey,
175
+ importContainerKey,
176
+ })
177
+ })
178
+ expect(onError).toHaveBeenCalledWith(jobError)
179
+ })
180
+
181
+ it('should delete container if container creation throws', async () => {
182
+ const onError = jest.fn()
183
+ const error = new Error('Container creation failed')
184
+ mockCreateImportContainerForFileUpload.mockRejectedValue(error)
185
+
186
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
187
+
188
+ await act(async () => {
189
+ await result.current.upload({
190
+ file: mockFile,
191
+ resourceType: 'product',
192
+ onSuccess: jest.fn(),
193
+ onError,
194
+ })
195
+ })
196
+
197
+ await waitFor(() => {
198
+ expect(mockDeleteImportContainer).toHaveBeenCalledWith({
199
+ projectKey,
200
+ importContainerKey,
201
+ })
202
+ })
203
+ expect(onError).toHaveBeenCalledWith(error)
204
+ })
205
+
206
+ it('should update progress during upload', async () => {
207
+ const onProgress = jest.fn()
208
+
209
+ mockCreateFileImportJob.mockImplementation(
210
+ ({ onProgress: _onProgress }) => {
211
+ _onProgress?.(50)
212
+ _onProgress?.(100)
213
+ return Promise.resolve(mockJobResponse)
214
+ }
215
+ )
216
+
217
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
218
+
219
+ await act(async () => {
220
+ await result.current.upload({
221
+ file: mockFile,
222
+ resourceType: 'product',
223
+ onSuccess: jest.fn(),
224
+ onProgress,
225
+ })
226
+ })
227
+
228
+ expect(onProgress).toHaveBeenCalledWith(50)
229
+ expect(onProgress).toHaveBeenCalledWith(100)
230
+ })
231
+
232
+ it('should update isUploading state correctly', async () => {
233
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
234
+
235
+ expect(result.current.isUploading).toBe(false)
236
+
237
+ await act(async () => {
238
+ await result.current.upload({
239
+ file: mockFile,
240
+ resourceType: 'product',
241
+ onSuccess: jest.fn(),
242
+ })
243
+ })
244
+
245
+ expect(result.current.isUploading).toBe(false)
246
+ expect(result.current.progress).toBe(100)
247
+ })
248
+
249
+ it('should work without optional settings', async () => {
250
+ const onSuccess = jest.fn()
251
+
252
+ const { result } = renderHook(() => useFileImportJobUpload({ projectKey }))
253
+
254
+ await act(async () => {
255
+ await result.current.upload({
256
+ file: mockFile,
257
+ resourceType: 'category',
258
+ onSuccess,
259
+ })
260
+ })
261
+
262
+ expect(mockCreateImportContainerForFileUpload).toHaveBeenCalledWith({
263
+ importContainerDraft: {
264
+ key: importContainerKey,
265
+ resourceType: 'category',
266
+ tags: ['source:file-upload'],
267
+ },
268
+ projectKey,
269
+ })
270
+
271
+ expect(onSuccess).toHaveBeenCalledWith('job-123', importContainerKey)
272
+ })
273
+ })
@@ -0,0 +1,101 @@
1
+ import React from 'react'
2
+ import type { ResourceTypeId } from '@commercetools/importapi-sdk'
3
+ import {
4
+ createFileImportJob,
5
+ createImportContainerForFileUpload,
6
+ deleteImportContainer,
7
+ } from '../@api'
8
+ import { ProjectKeyNotAvailableError } from '../@errors'
9
+ import { TAG_KEY_SOURCE_FILE_UPLOAD } from '../@constants'
10
+ import {
11
+ encodeFileNameWithTimestampToContainerKey,
12
+ getFileImportJobFileType,
13
+ } from '../@utils'
14
+ import type { ExtendedImportContainerDraft } from '../@types'
15
+
16
+ export type UseFileImportJobUploadConfig = {
17
+ file: File
18
+ resourceType: ResourceTypeId
19
+ settings?: ExtendedImportContainerDraft['settings']
20
+ onSuccess: (jobId: string, importContainerKey: string) => void
21
+ onError?: (error: unknown) => void
22
+ onProgress?: (progress: number) => void
23
+ abortSignal?: AbortSignal
24
+ }
25
+
26
+ export const useFileImportJobUpload = ({
27
+ projectKey,
28
+ }: {
29
+ projectKey: string
30
+ }) => {
31
+ const [isUploading, setIsUploading] = React.useState(false)
32
+ const [progress, setProgress] = React.useState(0)
33
+
34
+ const upload = React.useCallback(
35
+ async (config: UseFileImportJobUploadConfig) => {
36
+ if (!projectKey) {
37
+ throw new ProjectKeyNotAvailableError()
38
+ }
39
+
40
+ setIsUploading(true)
41
+ setProgress(0)
42
+
43
+ const importContainerKey = encodeFileNameWithTimestampToContainerKey(
44
+ config.file.name
45
+ )
46
+
47
+ try {
48
+ await createImportContainerForFileUpload({
49
+ importContainerDraft: {
50
+ key: importContainerKey,
51
+ resourceType: config.resourceType,
52
+ tags: [TAG_KEY_SOURCE_FILE_UPLOAD],
53
+ ...(config.settings ? { settings: config.settings } : {}),
54
+ },
55
+ projectKey,
56
+ })
57
+
58
+ const jobResponse = await createFileImportJob({
59
+ projectKey,
60
+ resourceType: config.resourceType,
61
+ importContainerKey,
62
+ payload: {
63
+ fileType: getFileImportJobFileType(config.resourceType),
64
+ fileName: config.file.name,
65
+ file: config.file,
66
+ },
67
+ onProgress: (uploadProgress) => {
68
+ setProgress(uploadProgress)
69
+ config.onProgress?.(uploadProgress)
70
+ },
71
+ abortSignal: config.abortSignal,
72
+ })
73
+
74
+ setIsUploading(false)
75
+ setProgress(100)
76
+ config.onSuccess(jobResponse.id, importContainerKey)
77
+ } catch (error) {
78
+ try {
79
+ await deleteImportContainer({
80
+ projectKey,
81
+ importContainerKey,
82
+ })
83
+ } catch {
84
+ // Ignore cleanup errors - container will be cleaned up by TTL retention policy
85
+ // Cleanup errors are unlikely unless there is a network issue or container was removed externally
86
+ }
87
+
88
+ setIsUploading(false)
89
+ setProgress(0)
90
+ config.onError?.(error)
91
+ }
92
+ },
93
+ [projectKey]
94
+ )
95
+
96
+ return {
97
+ upload,
98
+ isUploading,
99
+ progress,
100
+ }
101
+ }