@commercetools-frontend-extensions/operations 0.0.0-canary-20251209161906

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 (180) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +178 -0
  3. package/babel.config.js +6 -0
  4. package/dist/commercetools-frontend-extensions-operations.cjs.d.ts +2 -0
  5. package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +3273 -0
  6. package/dist/commercetools-frontend-extensions-operations.cjs.js +7 -0
  7. package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +3265 -0
  8. package/dist/commercetools-frontend-extensions-operations.esm.js +3074 -0
  9. package/dist/declarations/src/@api/export-operations.d.ts +5 -0
  10. package/dist/declarations/src/@api/fetcher.d.ts +17 -0
  11. package/dist/declarations/src/@api/file-import-jobs.d.ts +7 -0
  12. package/dist/declarations/src/@api/file-upload.d.ts +3 -0
  13. package/dist/declarations/src/@api/import-containers.d.ts +35 -0
  14. package/dist/declarations/src/@api/import-operations.d.ts +6 -0
  15. package/dist/declarations/src/@api/index.d.ts +9 -0
  16. package/dist/declarations/src/@api/process-file.d.ts +3 -0
  17. package/dist/declarations/src/@api/test-fixtures.d.ts +279 -0
  18. package/dist/declarations/src/@api/urls.d.ts +74 -0
  19. package/dist/declarations/src/@components/file-drop-area/active-drag-drop-area.d.ts +10 -0
  20. package/dist/declarations/src/@components/file-drop-area/disabled-drop-area.d.ts +5 -0
  21. package/dist/declarations/src/@components/file-drop-area/drop-area-wrapper.d.ts +11 -0
  22. package/dist/declarations/src/@components/file-drop-area/enabled-drop-area.d.ts +7 -0
  23. package/dist/declarations/src/@components/file-drop-area/file-drop-area.d.ts +14 -0
  24. package/dist/declarations/src/@components/file-drop-area/file-dropped-area.d.ts +6 -0
  25. package/dist/declarations/src/@components/file-drop-area/index.d.ts +7 -0
  26. package/dist/declarations/src/@components/file-drop-area/styles.d.ts +9 -0
  27. package/dist/declarations/src/@components/icons/file-icon.d.ts +2 -0
  28. package/dist/declarations/src/@components/icons/index.d.ts +2 -0
  29. package/dist/declarations/src/@components/icons/lock-icon.d.ts +2 -0
  30. package/dist/declarations/src/@components/index.d.ts +6 -0
  31. package/dist/declarations/src/@components/info-box/index.d.ts +1 -0
  32. package/dist/declarations/src/@components/info-box/info-box.d.ts +7 -0
  33. package/dist/declarations/src/@components/upload-separator/index.d.ts +1 -0
  34. package/dist/declarations/src/@components/upload-separator/upload-separator.d.ts +12 -0
  35. package/dist/declarations/src/@components/upload-settings/index.d.ts +1 -0
  36. package/dist/declarations/src/@components/upload-settings/upload-settings.d.ts +11 -0
  37. package/dist/declarations/src/@components/uploading-modal/index.d.ts +1 -0
  38. package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +13 -0
  39. package/dist/declarations/src/@constants/delimiters.d.ts +8 -0
  40. package/dist/declarations/src/@constants/file-import-job.d.ts +1 -0
  41. package/dist/declarations/src/@constants/import-limits.d.ts +6 -0
  42. package/dist/declarations/src/@constants/import-tags.d.ts +7 -0
  43. package/dist/declarations/src/@constants/index.d.ts +5 -0
  44. package/dist/declarations/src/@constants/resource-links.d.ts +10 -0
  45. package/dist/declarations/src/@errors/http-error.d.ts +6 -0
  46. package/dist/declarations/src/@errors/index.d.ts +9 -0
  47. package/dist/declarations/src/@errors/invalid-response-error.d.ts +3 -0
  48. package/dist/declarations/src/@errors/no-resources-to-export-error.d.ts +3 -0
  49. package/dist/declarations/src/@errors/polling-aborted-error.d.ts +3 -0
  50. package/dist/declarations/src/@errors/project-key-not-available-error.d.ts +3 -0
  51. package/dist/declarations/src/@errors/query-predicate-error.d.ts +4 -0
  52. package/dist/declarations/src/@errors/unexpected-column-error.d.ts +3 -0
  53. package/dist/declarations/src/@errors/unexpected-operation-state-error.d.ts +4 -0
  54. package/dist/declarations/src/@errors/unexpected-resource-type-error.d.ts +3 -0
  55. package/dist/declarations/src/@hooks/index.d.ts +8 -0
  56. package/dist/declarations/src/@hooks/use-fetch-export-operations.d.ts +15 -0
  57. package/dist/declarations/src/@hooks/use-fetch-file-import-job.d.ts +17 -0
  58. package/dist/declarations/src/@hooks/use-fetch-import-container-details.d.ts +15 -0
  59. package/dist/declarations/src/@hooks/use-fetch-import-operations.d.ts +16 -0
  60. package/dist/declarations/src/@hooks/use-fetch-import-summaries.d.ts +20 -0
  61. package/dist/declarations/src/@hooks/use-file-import-job-upload.d.ts +18 -0
  62. package/dist/declarations/src/@hooks/use-file-upload.d.ts +28 -0
  63. package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +19 -0
  64. package/dist/declarations/src/@types/api.d.ts +13 -0
  65. package/dist/declarations/src/@types/basic-error-data-type.d.ts +5 -0
  66. package/dist/declarations/src/@types/export-operation.d.ts +97 -0
  67. package/dist/declarations/src/@types/file-import-job.d.ts +99 -0
  68. package/dist/declarations/src/@types/file-upload-result.d.ts +21 -0
  69. package/dist/declarations/src/@types/file-upload.d.ts +63 -0
  70. package/dist/declarations/src/@types/import-container.d.ts +53 -0
  71. package/dist/declarations/src/@types/import-operation.d.ts +13 -0
  72. package/dist/declarations/src/@types/import-states.d.ts +9 -0
  73. package/dist/declarations/src/@types/import-summary.d.ts +15 -0
  74. package/dist/declarations/src/@types/index.d.ts +11 -0
  75. package/dist/declarations/src/@types/shared.d.ts +7 -0
  76. package/dist/declarations/src/@utils/error-mapping.d.ts +19 -0
  77. package/dist/declarations/src/@utils/file-import-job-helpers.d.ts +12 -0
  78. package/dist/declarations/src/@utils/file-upload.d.ts +54 -0
  79. package/dist/declarations/src/@utils/form.d.ts +1 -0
  80. package/dist/declarations/src/@utils/format.d.ts +5 -0
  81. package/dist/declarations/src/@utils/import-container.d.ts +8 -0
  82. package/dist/declarations/src/@utils/index.d.ts +8 -0
  83. package/dist/declarations/src/@utils/poll-job-until-validated.d.ts +11 -0
  84. package/dist/declarations/src/@utils/url.d.ts +6 -0
  85. package/dist/declarations/src/index.d.ts +26 -0
  86. package/index.js +1 -0
  87. package/jest.test.config.js +11 -0
  88. package/package.json +62 -0
  89. package/src/@api/export-operations.ts +26 -0
  90. package/src/@api/fetcher.spec.ts +51 -0
  91. package/src/@api/fetcher.ts +137 -0
  92. package/src/@api/file-import-jobs.ts +217 -0
  93. package/src/@api/file-upload.spec.ts +85 -0
  94. package/src/@api/file-upload.ts +46 -0
  95. package/src/@api/import-containers.ts +256 -0
  96. package/src/@api/import-operations.ts +33 -0
  97. package/src/@api/index.ts +9 -0
  98. package/src/@api/process-file.spec.ts +74 -0
  99. package/src/@api/process-file.ts +53 -0
  100. package/src/@api/test-fixtures.ts +894 -0
  101. package/src/@api/urls.ts +194 -0
  102. package/src/@components/file-drop-area/active-drag-drop-area.tsx +33 -0
  103. package/src/@components/file-drop-area/disabled-drop-area.tsx +17 -0
  104. package/src/@components/file-drop-area/drop-area-wrapper.tsx +38 -0
  105. package/src/@components/file-drop-area/enabled-drop-area.tsx +27 -0
  106. package/src/@components/file-drop-area/file-drop-area.tsx +74 -0
  107. package/src/@components/file-drop-area/file-dropped-area.tsx +29 -0
  108. package/src/@components/file-drop-area/index.ts +7 -0
  109. package/src/@components/file-drop-area/styles.ts +67 -0
  110. package/src/@components/icons/file-icon.tsx +30 -0
  111. package/src/@components/icons/index.ts +2 -0
  112. package/src/@components/icons/lock-icon.tsx +34 -0
  113. package/src/@components/index.ts +6 -0
  114. package/src/@components/info-box/index.ts +1 -0
  115. package/src/@components/info-box/info-box.tsx +23 -0
  116. package/src/@components/upload-separator/index.ts +1 -0
  117. package/src/@components/upload-separator/upload-separator.tsx +61 -0
  118. package/src/@components/upload-settings/index.ts +1 -0
  119. package/src/@components/upload-settings/upload-settings.tsx +36 -0
  120. package/src/@components/uploading-modal/index.ts +1 -0
  121. package/src/@components/uploading-modal/uploading-modal.tsx +66 -0
  122. package/src/@constants/delimiters.ts +14 -0
  123. package/src/@constants/file-import-job.ts +1 -0
  124. package/src/@constants/import-limits.ts +13 -0
  125. package/src/@constants/import-tags.ts +9 -0
  126. package/src/@constants/index.ts +5 -0
  127. package/src/@constants/resource-links.ts +61 -0
  128. package/src/@errors/http-error.ts +17 -0
  129. package/src/@errors/index.ts +9 -0
  130. package/src/@errors/invalid-response-error.ts +6 -0
  131. package/src/@errors/no-resources-to-export-error.ts +6 -0
  132. package/src/@errors/polling-aborted-error.ts +6 -0
  133. package/src/@errors/project-key-not-available-error.ts +6 -0
  134. package/src/@errors/query-predicate-error.ts +10 -0
  135. package/src/@errors/unexpected-column-error.ts +6 -0
  136. package/src/@errors/unexpected-operation-state-error.ts +8 -0
  137. package/src/@errors/unexpected-resource-type-error.ts +6 -0
  138. package/src/@hooks/index.ts +8 -0
  139. package/src/@hooks/use-fetch-export-operations.ts +34 -0
  140. package/src/@hooks/use-fetch-file-import-job.spec.ts +131 -0
  141. package/src/@hooks/use-fetch-file-import-job.ts +38 -0
  142. package/src/@hooks/use-fetch-import-container-details.ts +31 -0
  143. package/src/@hooks/use-fetch-import-operations.ts +42 -0
  144. package/src/@hooks/use-fetch-import-summaries.ts +47 -0
  145. package/src/@hooks/use-fetch.spec.ts +68 -0
  146. package/src/@hooks/use-fetch.ts +76 -0
  147. package/src/@hooks/use-file-import-job-upload.spec.ts +273 -0
  148. package/src/@hooks/use-file-import-job-upload.ts +101 -0
  149. package/src/@hooks/use-file-upload.ts +223 -0
  150. package/src/@hooks/use-import-container-upload.spec.ts +297 -0
  151. package/src/@hooks/use-import-container-upload.ts +130 -0
  152. package/src/@types/api.ts +14 -0
  153. package/src/@types/basic-error-data-type.ts +5 -0
  154. package/src/@types/export-operation.ts +147 -0
  155. package/src/@types/file-import-job.ts +165 -0
  156. package/src/@types/file-upload-result.ts +23 -0
  157. package/src/@types/file-upload.ts +81 -0
  158. package/src/@types/import-container.ts +104 -0
  159. package/src/@types/import-operation.ts +31 -0
  160. package/src/@types/import-states.ts +9 -0
  161. package/src/@types/import-summary.ts +22 -0
  162. package/src/@types/index.ts +11 -0
  163. package/src/@types/shared.ts +52 -0
  164. package/src/@utils/error-mapping.spec.ts +126 -0
  165. package/src/@utils/error-mapping.ts +40 -0
  166. package/src/@utils/file-import-job-helpers.spec.ts +147 -0
  167. package/src/@utils/file-import-job-helpers.ts +47 -0
  168. package/src/@utils/file-upload.spec.ts +151 -0
  169. package/src/@utils/file-upload.ts +189 -0
  170. package/src/@utils/form.ts +20 -0
  171. package/src/@utils/format.spec.ts +62 -0
  172. package/src/@utils/format.ts +53 -0
  173. package/src/@utils/import-container.spec.ts +26 -0
  174. package/src/@utils/import-container.ts +34 -0
  175. package/src/@utils/index.ts +8 -0
  176. package/src/@utils/poll-job-until-validated.ts +76 -0
  177. package/src/@utils/url.spec.ts +75 -0
  178. package/src/@utils/url.ts +18 -0
  179. package/src/index.ts +27 -0
  180. package/tsconfig.json +9 -0
@@ -0,0 +1,68 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react'
2
+ import { HttpError } from '../@errors'
3
+ import { useFetch } from './use-fetch'
4
+
5
+ jest.mock('@commercetools-frontend/sentry', () => ({
6
+ reportErrorToSentry: jest.fn(),
7
+ }))
8
+
9
+ describe('useFetch', () => {
10
+ const mockFetchFunction = jest.fn()
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+ })
15
+ it('should fetch data successfully', async () => {
16
+ const data = { data: 'some data' }
17
+ mockFetchFunction.mockResolvedValueOnce(data)
18
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
19
+ expect(result.current.isLoading).toBe(true)
20
+ await waitFor(() => {
21
+ expect(result.current.isLoading).toBe(false)
22
+ })
23
+
24
+ expect(result.current.data).toEqual(data)
25
+ expect(result.current.error).toBeNull()
26
+ })
27
+ it('should handle HttpError', async () => {
28
+ const error = new HttpError(500, 'Http error')
29
+ mockFetchFunction.mockRejectedValueOnce(error)
30
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
31
+ await waitFor(() => {
32
+ expect(result.current.isLoading).toBe(false)
33
+ })
34
+ expect(result.current.data).toBeNull()
35
+ expect(result.current.error).toEqual(error)
36
+ })
37
+ it('should refetch data', async () => {
38
+ const data = { data: 'some data' }
39
+ mockFetchFunction.mockResolvedValueOnce(data)
40
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
41
+ await waitFor(() => {
42
+ expect(result.current.data).toEqual(data)
43
+ })
44
+ const newData = { data: 'new data' }
45
+ mockFetchFunction.mockResolvedValueOnce(newData)
46
+ act(() => {
47
+ result.current.refetch()
48
+ })
49
+ await waitFor(() => {
50
+ expect(result.current.data).toEqual(newData)
51
+ })
52
+ })
53
+ it('should handle polling', async () => {
54
+ const data = { data: 'some data' }
55
+ mockFetchFunction.mockResolvedValue(data)
56
+ const { result } = renderHook(() =>
57
+ useFetch(mockFetchFunction, { pollingInterval: 50 })
58
+ )
59
+ await waitFor(() => {
60
+ expect(result.current.data).toEqual(data)
61
+ })
62
+ const newData = { data: 'new data' }
63
+ mockFetchFunction.mockResolvedValue(newData)
64
+ await waitFor(() => {
65
+ expect(result.current.data).toEqual(newData)
66
+ })
67
+ })
68
+ })
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import { reportErrorToSentry } from '@commercetools-frontend/sentry'
3
+ import { HttpError } from '../@errors'
4
+
5
+ export const useFetch = <Data>(
6
+ fetchFunction: () => Promise<Data>,
7
+ config: {
8
+ pollingInterval?: number
9
+ shouldContinuePolling?: (data: Data) => boolean
10
+ } = {}
11
+ ) => {
12
+ const [data, setData] = React.useState<Data | null>(null)
13
+ const [error, setError] = React.useState<Error | null>(null)
14
+ const [isLoading, setIsLoading] = React.useState(false)
15
+ const [refetchCount, setRefetchCount] = React.useState(0)
16
+ const [lastFetchTime, setLastFetchTime] = React.useState<Date>(new Date())
17
+
18
+ const refetch = React.useCallback(() => {
19
+ setRefetchCount((count) => count + 1)
20
+ }, [])
21
+
22
+ React.useEffect(() => {
23
+ let pollingId: NodeJS.Timeout | null = null
24
+ const fetchData = async () => {
25
+ setIsLoading(true)
26
+ try {
27
+ const response = await fetchFunction()
28
+ setData(response)
29
+ setLastFetchTime(new Date())
30
+ if (
31
+ config.shouldContinuePolling &&
32
+ !config.shouldContinuePolling(response)
33
+ ) {
34
+ if (pollingId) {
35
+ clearInterval(pollingId)
36
+ pollingId = null
37
+ }
38
+ }
39
+ } catch (err) {
40
+ const error = err instanceof Error ? err : new Error(String(err))
41
+ setError(error)
42
+
43
+ if (!(err instanceof HttpError)) {
44
+ reportErrorToSentry(
45
+ new Error('An unexpected error occurred in the `useFetch` hook'),
46
+ {
47
+ extra: {
48
+ error: err,
49
+ },
50
+ }
51
+ )
52
+ }
53
+ } finally {
54
+ setIsLoading(false)
55
+ }
56
+ }
57
+ fetchData()
58
+ if (config.pollingInterval && config.pollingInterval > 0) {
59
+ pollingId = setInterval(fetchData, config.pollingInterval)
60
+ }
61
+ return () => {
62
+ if (pollingId) {
63
+ clearInterval(pollingId)
64
+ }
65
+ setError(null)
66
+ setIsLoading(false)
67
+ }
68
+ }, [
69
+ fetchFunction,
70
+ refetchCount,
71
+ config.pollingInterval,
72
+ config.shouldContinuePolling,
73
+ ])
74
+
75
+ return { data, error, isLoading, refetch, lastFetchTime }
76
+ }
@@ -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
+ }