@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +169 -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 +2469 -0
  6. package/dist/commercetools-frontend-extensions-operations.cjs.js +7 -0
  7. package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +2461 -0
  8. package/dist/commercetools-frontend-extensions-operations.esm.js +2316 -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-upload.d.ts +3 -0
  12. package/dist/declarations/src/@api/import-containers.d.ts +35 -0
  13. package/dist/declarations/src/@api/import-operations.d.ts +6 -0
  14. package/dist/declarations/src/@api/index.d.ts +8 -0
  15. package/dist/declarations/src/@api/process-file.d.ts +3 -0
  16. package/dist/declarations/src/@api/test-fixtures.d.ts +272 -0
  17. package/dist/declarations/src/@api/urls.d.ts +44 -0
  18. package/dist/declarations/src/@components/file-drop-area/active-drag-drop-area.d.ts +10 -0
  19. package/dist/declarations/src/@components/file-drop-area/disabled-drop-area.d.ts +5 -0
  20. package/dist/declarations/src/@components/file-drop-area/drop-area-wrapper.d.ts +11 -0
  21. package/dist/declarations/src/@components/file-drop-area/enabled-drop-area.d.ts +7 -0
  22. package/dist/declarations/src/@components/file-drop-area/file-drop-area.d.ts +14 -0
  23. package/dist/declarations/src/@components/file-drop-area/file-dropped-area.d.ts +6 -0
  24. package/dist/declarations/src/@components/file-drop-area/index.d.ts +7 -0
  25. package/dist/declarations/src/@components/file-drop-area/styles.d.ts +9 -0
  26. package/dist/declarations/src/@components/icons/file-icon.d.ts +2 -0
  27. package/dist/declarations/src/@components/icons/index.d.ts +2 -0
  28. package/dist/declarations/src/@components/icons/lock-icon.d.ts +2 -0
  29. package/dist/declarations/src/@components/index.d.ts +6 -0
  30. package/dist/declarations/src/@components/info-box/index.d.ts +1 -0
  31. package/dist/declarations/src/@components/info-box/info-box.d.ts +7 -0
  32. package/dist/declarations/src/@components/upload-separator/index.d.ts +1 -0
  33. package/dist/declarations/src/@components/upload-separator/upload-separator.d.ts +12 -0
  34. package/dist/declarations/src/@components/upload-settings/index.d.ts +1 -0
  35. package/dist/declarations/src/@components/upload-settings/upload-settings.d.ts +11 -0
  36. package/dist/declarations/src/@components/uploading-modal/index.d.ts +1 -0
  37. package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +12 -0
  38. package/dist/declarations/src/@constants/delimiters.d.ts +8 -0
  39. package/dist/declarations/src/@constants/import-tags.d.ts +7 -0
  40. package/dist/declarations/src/@constants/index.d.ts +4 -0
  41. package/dist/declarations/src/@constants/resource-links.d.ts +10 -0
  42. package/dist/declarations/src/@constants/upload-limits.d.ts +10 -0
  43. package/dist/declarations/src/@errors/http-error.d.ts +6 -0
  44. package/dist/declarations/src/@errors/index.d.ts +8 -0
  45. package/dist/declarations/src/@errors/invalid-response-error.d.ts +3 -0
  46. package/dist/declarations/src/@errors/no-resources-to-export-error.d.ts +3 -0
  47. package/dist/declarations/src/@errors/project-key-not-available-error.d.ts +3 -0
  48. package/dist/declarations/src/@errors/query-predicate-error.d.ts +4 -0
  49. package/dist/declarations/src/@errors/unexpected-column-error.d.ts +3 -0
  50. package/dist/declarations/src/@errors/unexpected-operation-state-error.d.ts +4 -0
  51. package/dist/declarations/src/@errors/unexpected-resource-type-error.d.ts +3 -0
  52. package/dist/declarations/src/@hooks/index.d.ts +5 -0
  53. package/dist/declarations/src/@hooks/use-fetch-export-operations.d.ts +15 -0
  54. package/dist/declarations/src/@hooks/use-fetch-import-container-details.d.ts +15 -0
  55. package/dist/declarations/src/@hooks/use-fetch-import-operations.d.ts +16 -0
  56. package/dist/declarations/src/@hooks/use-fetch-import-summaries.d.ts +20 -0
  57. package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +18 -0
  58. package/dist/declarations/src/@types/api.d.ts +13 -0
  59. package/dist/declarations/src/@types/basic-error-data-type.d.ts +5 -0
  60. package/dist/declarations/src/@types/export-operation.d.ts +95 -0
  61. package/dist/declarations/src/@types/file-upload.d.ts +63 -0
  62. package/dist/declarations/src/@types/import-container.d.ts +53 -0
  63. package/dist/declarations/src/@types/import-operation.d.ts +13 -0
  64. package/dist/declarations/src/@types/import-states.d.ts +9 -0
  65. package/dist/declarations/src/@types/import-summary.d.ts +15 -0
  66. package/dist/declarations/src/@types/index.d.ts +9 -0
  67. package/dist/declarations/src/@types/shared.d.ts +7 -0
  68. package/dist/declarations/src/@utils/error-mapping.d.ts +19 -0
  69. package/dist/declarations/src/@utils/file-upload.d.ts +46 -0
  70. package/dist/declarations/src/@utils/form.d.ts +1 -0
  71. package/dist/declarations/src/@utils/format.d.ts +5 -0
  72. package/dist/declarations/src/@utils/import-container.d.ts +8 -0
  73. package/dist/declarations/src/@utils/index.d.ts +6 -0
  74. package/dist/declarations/src/@utils/url.d.ts +6 -0
  75. package/dist/declarations/src/index.d.ts +26 -0
  76. package/index.js +1 -0
  77. package/jest.test.config.js +11 -0
  78. package/package.json +63 -0
  79. package/src/@api/export-operations.ts +26 -0
  80. package/src/@api/fetcher.spec.ts +51 -0
  81. package/src/@api/fetcher.ts +127 -0
  82. package/src/@api/file-upload.spec.ts +83 -0
  83. package/src/@api/file-upload.ts +46 -0
  84. package/src/@api/import-containers.ts +256 -0
  85. package/src/@api/import-operations.ts +33 -0
  86. package/src/@api/index.ts +8 -0
  87. package/src/@api/process-file.spec.ts +74 -0
  88. package/src/@api/process-file.ts +53 -0
  89. package/src/@api/test-fixtures.ts +772 -0
  90. package/src/@api/urls.ts +118 -0
  91. package/src/@components/file-drop-area/active-drag-drop-area.tsx +33 -0
  92. package/src/@components/file-drop-area/disabled-drop-area.tsx +17 -0
  93. package/src/@components/file-drop-area/drop-area-wrapper.tsx +38 -0
  94. package/src/@components/file-drop-area/enabled-drop-area.tsx +27 -0
  95. package/src/@components/file-drop-area/file-drop-area.tsx +74 -0
  96. package/src/@components/file-drop-area/file-dropped-area.tsx +29 -0
  97. package/src/@components/file-drop-area/index.ts +7 -0
  98. package/src/@components/file-drop-area/styles.ts +67 -0
  99. package/src/@components/icons/file-icon.tsx +30 -0
  100. package/src/@components/icons/index.ts +2 -0
  101. package/src/@components/icons/lock-icon.tsx +34 -0
  102. package/src/@components/index.ts +6 -0
  103. package/src/@components/info-box/index.ts +1 -0
  104. package/src/@components/info-box/info-box.tsx +23 -0
  105. package/src/@components/upload-separator/index.ts +1 -0
  106. package/src/@components/upload-separator/upload-separator.tsx +61 -0
  107. package/src/@components/upload-settings/index.ts +1 -0
  108. package/src/@components/upload-settings/upload-settings.tsx +36 -0
  109. package/src/@components/uploading-modal/index.ts +1 -0
  110. package/src/@components/uploading-modal/uploading-modal.tsx +64 -0
  111. package/src/@constants/delimiters.ts +14 -0
  112. package/src/@constants/import-tags.ts +9 -0
  113. package/src/@constants/index.ts +4 -0
  114. package/src/@constants/resource-links.ts +61 -0
  115. package/src/@constants/upload-limits.ts +11 -0
  116. package/src/@errors/http-error.ts +17 -0
  117. package/src/@errors/index.ts +8 -0
  118. package/src/@errors/invalid-response-error.ts +6 -0
  119. package/src/@errors/no-resources-to-export-error.ts +6 -0
  120. package/src/@errors/project-key-not-available-error.ts +6 -0
  121. package/src/@errors/query-predicate-error.ts +10 -0
  122. package/src/@errors/unexpected-column-error.ts +6 -0
  123. package/src/@errors/unexpected-operation-state-error.ts +8 -0
  124. package/src/@errors/unexpected-resource-type-error.ts +6 -0
  125. package/src/@hooks/index.ts +5 -0
  126. package/src/@hooks/messages.ts +11 -0
  127. package/src/@hooks/use-fetch-export-operations.ts +34 -0
  128. package/src/@hooks/use-fetch-import-container-details.ts +31 -0
  129. package/src/@hooks/use-fetch-import-operations.ts +42 -0
  130. package/src/@hooks/use-fetch-import-summaries.ts +47 -0
  131. package/src/@hooks/use-fetch.spec.ts +76 -0
  132. package/src/@hooks/use-fetch.ts +80 -0
  133. package/src/@hooks/use-import-container-upload.spec.ts +294 -0
  134. package/src/@hooks/use-import-container-upload.ts +126 -0
  135. package/src/@types/api.ts +14 -0
  136. package/src/@types/basic-error-data-type.ts +5 -0
  137. package/src/@types/export-operation.ts +144 -0
  138. package/src/@types/file-upload.ts +81 -0
  139. package/src/@types/import-container.ts +104 -0
  140. package/src/@types/import-operation.ts +31 -0
  141. package/src/@types/import-states.ts +9 -0
  142. package/src/@types/import-summary.ts +22 -0
  143. package/src/@types/index.ts +9 -0
  144. package/src/@types/shared.ts +52 -0
  145. package/src/@utils/error-mapping.spec.ts +126 -0
  146. package/src/@utils/error-mapping.ts +39 -0
  147. package/src/@utils/file-upload.spec.ts +151 -0
  148. package/src/@utils/file-upload.ts +150 -0
  149. package/src/@utils/form.ts +20 -0
  150. package/src/@utils/format.spec.ts +62 -0
  151. package/src/@utils/format.ts +53 -0
  152. package/src/@utils/import-container.spec.ts +26 -0
  153. package/src/@utils/import-container.ts +34 -0
  154. package/src/@utils/index.ts +6 -0
  155. package/src/@utils/url.spec.ts +75 -0
  156. package/src/@utils/url.ts +18 -0
  157. package/src/index.ts +27 -0
  158. package/tsconfig.json +9 -0
@@ -0,0 +1,64 @@
1
+ import { useIntl } from 'react-intl'
2
+ import { InfoDialog } from '@commercetools-frontend/application-components'
3
+ import {
4
+ Constraints,
5
+ ProgressBar,
6
+ SecondaryButton,
7
+ Spacings,
8
+ Text,
9
+ } from '@commercetools-frontend/ui-kit'
10
+ import { convertFileSizeToKB } from '../../@utils'
11
+ import { FileIcon } from '../icons/file-icon'
12
+
13
+ type UploadingModalProps = {
14
+ isOpen: boolean
15
+ title: string
16
+ fileName: string
17
+ fileSize: number
18
+ progress: number
19
+ cancelLabel: string
20
+ onCancel: () => void
21
+ onClose: () => void
22
+ }
23
+
24
+ export const UploadingModal = ({
25
+ isOpen,
26
+ title,
27
+ fileName,
28
+ fileSize,
29
+ progress,
30
+ cancelLabel,
31
+ onCancel,
32
+ onClose,
33
+ }: UploadingModalProps) => {
34
+ const intl = useIntl()
35
+ return (
36
+ <InfoDialog size={16} isOpen={isOpen} title={title} onClose={onClose}>
37
+ <Spacings.Stack scale="m">
38
+ <Spacings.Inline alignItems="center" justifyContent="space-between">
39
+ <Spacings.Inline alignItems="center">
40
+ <FileIcon />
41
+ <Spacings.Inline>
42
+ <Constraints.Horizontal max={10}>
43
+ <Text.Body truncate isBold>
44
+ {fileName}
45
+ </Text.Body>
46
+ </Constraints.Horizontal>
47
+ </Spacings.Inline>
48
+ <Text.Body tone="secondary">
49
+ ({intl.formatNumber(convertFileSizeToKB(fileSize))} KB)
50
+ </Text.Body>
51
+ </Spacings.Inline>
52
+ <SecondaryButton
53
+ tone="secondary"
54
+ size="medium"
55
+ label={cancelLabel}
56
+ onClick={onCancel}
57
+ />
58
+ </Spacings.Inline>
59
+ <ProgressBar barWidth="scale" height="10" progress={progress} />
60
+ <div />
61
+ </Spacings.Stack>
62
+ </InfoDialog>
63
+ )
64
+ }
@@ -0,0 +1,14 @@
1
+ export const DELIMITERS = {
2
+ COMMA: ',',
3
+ SEMICOLON: ';',
4
+ POINT: '.',
5
+ TAB: '\t',
6
+ PIPE: '|',
7
+ }
8
+
9
+ export const COLUMN_DELIMITERS = [
10
+ DELIMITERS.COMMA,
11
+ DELIMITERS.SEMICOLON,
12
+ DELIMITERS.PIPE,
13
+ DELIMITERS.TAB,
14
+ ]
@@ -0,0 +1,9 @@
1
+ export const IMPORT_TAG_KEYS = {
2
+ source: 'source',
3
+ }
4
+
5
+ export const IMPORT_TAG_VALUES = {
6
+ fileUpload: 'file-upload',
7
+ }
8
+
9
+ export const TAG_KEY_SOURCE_FILE_UPLOAD = `${IMPORT_TAG_KEYS.source}:${IMPORT_TAG_VALUES.fileUpload}`
@@ -0,0 +1,4 @@
1
+ export * from './delimiters'
2
+ export * from './import-tags'
3
+ export * from './resource-links'
4
+ export * from './upload-limits'
@@ -0,0 +1,61 @@
1
+ import type { ResourceTypeId } from '@commercetools/importapi-sdk'
2
+
3
+ /**
4
+ * CommerceTools API documentation base URL
5
+ */
6
+ export const CT_API_DOCS_URL = 'https://docs.commercetools.com/api/'
7
+
8
+ /**
9
+ * Template download links for each resource type
10
+ */
11
+ export const RESOURCE_TYPE_TEMPLATE_DOWNLOAD_LINKS: Record<
12
+ ResourceTypeId,
13
+ string
14
+ > = {
15
+ category:
16
+ 'https://docs.commercetools.com/merchant-center/downloads/category_import_template.csv',
17
+ 'custom-object':
18
+ 'https://docs.commercetools.com/merchant-center/downloads/custom_object_import_template.json',
19
+ product:
20
+ 'https://docs.commercetools.com/merchant-center/import-data#download-a-template',
21
+ 'inventory-entry':
22
+ 'https://docs.commercetools.com/merchant-center/downloads/inventory_entry_import_template.csv',
23
+ // TODO: remove `inventory` after aligning the resource type names in the Import API and Export API
24
+ inventory:
25
+ 'https://docs.commercetools.com/merchant-center/downloads/inventory_entry_import_template.csv',
26
+ 'discount-code':
27
+ 'https://docs.commercetools.com/merchant-center/downloads/discount_code_import_template.csv',
28
+ customer:
29
+ 'https://docs.commercetools.com/merchant-center/import-data#download-a-template',
30
+ order:
31
+ 'https://docs.commercetools.com/merchant-center/import-data#download-a-template',
32
+ 'product-type':
33
+ 'https://docs.commercetools.com/merchant-center/downloads/product_type_import_template.csv',
34
+ 'business-unit':
35
+ 'https://docs.commercetools.com/merchant-center/downloads/business_unit_import_template.csv',
36
+ }
37
+
38
+ export const RESOURCE_TYPE_DOCUMENTATION_LINKS: Record<ResourceTypeId, string> =
39
+ {
40
+ category:
41
+ 'https://docs.commercetools.com/merchant-center/import-categories#supported-headers-and-values',
42
+ 'custom-object':
43
+ 'https://docs.commercetools.com/merchant-center/import-custom-objects#supported-fields-and-values',
44
+ product:
45
+ 'https://docs.commercetools.com/merchant-center/import-products#supported-headers-and-values',
46
+ 'inventory-entry':
47
+ 'https://docs.commercetools.com/merchant-center/import-inventory#supported-headers-and-values',
48
+ // TODO: remove `inventory` after aligning the resource type names in the Import API and Export API
49
+ inventory:
50
+ 'https://docs.commercetools.com/merchant-center/import-inventory#supported-headers-and-values',
51
+ 'discount-code':
52
+ 'https://docs.commercetools.com/merchant-center/import-discount-codes#supported-headers-and-values',
53
+ customer:
54
+ 'https://docs.commercetools.com/merchant-center/import-customers#supported-headers-and-values',
55
+ order:
56
+ 'https://docs.commercetools.com/merchant-center/import-orders#supported-headers-and-values',
57
+ 'product-type':
58
+ 'https://docs.commercetools.com/merchant-center/import-product-types#supported-headers-and-values',
59
+ 'business-unit':
60
+ 'https://docs.commercetools.com/merchant-center/import-business-units#supported-headers-and-values',
61
+ }
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,17 @@
1
+ import type { BasicErrorDataType } from '../@types'
2
+
3
+ export class HttpError<T = BasicErrorDataType> extends Error {
4
+ readonly statusCode: number
5
+ readonly errorData?: T
6
+
7
+ constructor(statusCode: number, statusText?: string, errorData?: T) {
8
+ super(
9
+ `HTTP Error! Status code: ${statusCode}, message: "${
10
+ statusText ? statusText : ''
11
+ }"`
12
+ )
13
+ this.name = 'HttpError'
14
+ this.statusCode = statusCode
15
+ this.errorData = errorData
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ export * from './unexpected-column-error'
2
+ export * from './unexpected-operation-state-error'
3
+ export * from './unexpected-resource-type-error'
4
+ export * from './http-error'
5
+ export * from './invalid-response-error'
6
+ export * from './query-predicate-error'
7
+ export * from './no-resources-to-export-error'
8
+ export * from './project-key-not-available-error'
@@ -0,0 +1,6 @@
1
+ export class InvalidResponseError extends Error {
2
+ constructor(message: string) {
3
+ super(message)
4
+ this.name = 'InvalidResponseError'
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export class NoResourcesToExportError extends Error {
2
+ constructor(message: string = 'There are no resources to export.') {
3
+ super(message)
4
+ this.name = 'NoResourcesToExportError'
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export class ProjectKeyNotAvailableError extends Error {
2
+ constructor(message: string = 'Project key is not available') {
3
+ super(message)
4
+ this.name = 'ProjectKeyNotAvailableError'
5
+ }
6
+ }
@@ -0,0 +1,10 @@
1
+ export class QueryPredicateError extends Error {
2
+ field = 'queryPredicate'
3
+
4
+ constructor(
5
+ message: string = 'There is an error with the query predicate. Make sure the syntax is correct.'
6
+ ) {
7
+ super(message)
8
+ this.name = 'QueryPredicateError'
9
+ }
10
+ }
@@ -0,0 +1,6 @@
1
+ export class UnexpectedColumnError extends Error {
2
+ constructor(columnName: string) {
3
+ super(`Unexpected column "${columnName}"`)
4
+ this.name = 'UnexpectedColumnError'
5
+ }
6
+ }
@@ -0,0 +1,8 @@
1
+ import { ProcessingState } from '@commercetools/importapi-sdk'
2
+
3
+ export class UnexpectedOperationStateError extends Error {
4
+ constructor(state: ProcessingState) {
5
+ super(`Unexpected operation state "${state}"`)
6
+ this.name = 'UnexpectedOperationStateError'
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ export class UnexpectedResourceTypeError extends Error {
2
+ constructor(resourceType: string) {
3
+ super(`Unexpected resource type "${resourceType}"`)
4
+ this.name = 'UnexpectedResourceTypeError'
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ export * from './use-fetch-export-operations'
2
+ export * from './use-fetch-import-container-details'
3
+ export * from './use-fetch-import-operations'
4
+ export * from './use-fetch-import-summaries'
5
+ export * from './use-import-container-upload'
@@ -0,0 +1,11 @@
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
+ })
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+ import { fetchExportOperations } from '../@api'
3
+ import { ProjectKeyNotAvailableError } from '../@errors'
4
+ import type {
5
+ PaginatedExportOperationResponse,
6
+ ExportOperationQueryParams,
7
+ } from '../@types'
8
+ import { useFetch } from './use-fetch'
9
+
10
+ type UseFetchExportOperationsConfig = {
11
+ projectKey: string
12
+ queryParams: ExportOperationQueryParams
13
+ pollingInterval?: number
14
+ shouldContinuePolling?: (data: PaginatedExportOperationResponse) => boolean
15
+ }
16
+
17
+ export const useFetchExportOperations = ({
18
+ projectKey,
19
+ queryParams,
20
+ pollingInterval,
21
+ shouldContinuePolling,
22
+ }: UseFetchExportOperationsConfig) => {
23
+ const fetchData = React.useCallback(() => {
24
+ if (!projectKey) {
25
+ return Promise.reject(new ProjectKeyNotAvailableError())
26
+ }
27
+ return fetchExportOperations({ projectKey, queryParams })
28
+ }, [projectKey, queryParams])
29
+
30
+ return useFetch(fetchData, {
31
+ pollingInterval,
32
+ shouldContinuePolling,
33
+ })
34
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import { fetchImportContainerDetails } from '../@api'
3
+ import { ProjectKeyNotAvailableError } from '../@errors'
4
+ import { useFetch } from './use-fetch'
5
+ import { type ImportContainerDetails } from '../@types'
6
+
7
+ type UseFetchImportContainerDetailsConfig = {
8
+ projectKey: string
9
+ importContainerKey: string
10
+ pollingInterval?: number
11
+ shouldContinuePolling?: (data: ImportContainerDetails) => boolean
12
+ }
13
+
14
+ export const useFetchImportContainerDetails = ({
15
+ projectKey,
16
+ importContainerKey,
17
+ pollingInterval,
18
+ shouldContinuePolling,
19
+ }: UseFetchImportContainerDetailsConfig) => {
20
+ const fetchData = React.useCallback(() => {
21
+ if (!projectKey) {
22
+ return Promise.reject(new ProjectKeyNotAvailableError())
23
+ }
24
+ return fetchImportContainerDetails({ projectKey, importContainerKey })
25
+ }, [projectKey, importContainerKey])
26
+
27
+ return useFetch(fetchData, {
28
+ pollingInterval,
29
+ shouldContinuePolling,
30
+ })
31
+ }
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+ import { fetchImportOperations } from '../@api'
3
+ import { ProjectKeyNotAvailableError } from '../@errors'
4
+ import type {
5
+ ExtendedImportOperationPagedResponse,
6
+ ImportOperationQueryParams,
7
+ } from '../@types'
8
+ import { useFetch } from './use-fetch'
9
+
10
+ type UseFetchImportOperationsConfig = {
11
+ projectKey: string
12
+ importContainerKey: string
13
+ queryParams: ImportOperationQueryParams
14
+ pollingInterval?: number
15
+ shouldContinuePolling?: (
16
+ data: ExtendedImportOperationPagedResponse
17
+ ) => boolean
18
+ }
19
+
20
+ export const useFetchImportOperations = ({
21
+ projectKey,
22
+ importContainerKey,
23
+ queryParams,
24
+ pollingInterval,
25
+ shouldContinuePolling,
26
+ }: UseFetchImportOperationsConfig) => {
27
+ const fetchData = React.useCallback(() => {
28
+ if (!projectKey) {
29
+ return Promise.reject(new ProjectKeyNotAvailableError())
30
+ }
31
+ return fetchImportOperations({
32
+ projectKey,
33
+ importContainerKey,
34
+ queryParams,
35
+ })
36
+ }, [projectKey, importContainerKey, queryParams])
37
+
38
+ return useFetch(fetchData, {
39
+ pollingInterval,
40
+ shouldContinuePolling,
41
+ })
42
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import { fetchImportSummaries } from '../@api'
3
+ import { ProjectKeyNotAvailableError } from '../@errors'
4
+ import type {
5
+ ImportContainerQueryParams,
6
+ ImportContainerDetails,
7
+ } from '../@types'
8
+ import { useFetch } from './use-fetch'
9
+
10
+ export type ImportSummariesResult = {
11
+ results: ImportContainerDetails[]
12
+ count: number
13
+ total: number
14
+ }
15
+
16
+ type UseFetchImportSummariesConfig = {
17
+ projectKey: string
18
+ queryParams: ImportContainerQueryParams
19
+ pollingInterval?: number
20
+ shouldContinuePolling?: (data: ImportSummariesResult) => boolean
21
+ }
22
+
23
+ export const useFetchImportSummaries = ({
24
+ projectKey,
25
+ queryParams,
26
+ pollingInterval,
27
+ shouldContinuePolling,
28
+ }: UseFetchImportSummariesConfig) => {
29
+ const fetchData =
30
+ React.useCallback(async (): Promise<ImportSummariesResult> => {
31
+ if (!projectKey) {
32
+ return Promise.reject(new ProjectKeyNotAvailableError())
33
+ }
34
+ const summary = await fetchImportSummaries({ projectKey, queryParams })
35
+ const resolvedResults = await Promise.all(summary.results)
36
+ return {
37
+ results: resolvedResults,
38
+ count: summary.count,
39
+ total: summary.total,
40
+ }
41
+ }, [projectKey, queryParams])
42
+
43
+ return useFetch<ImportSummariesResult>(fetchData, {
44
+ pollingInterval,
45
+ shouldContinuePolling,
46
+ })
47
+ }
@@ -0,0 +1,76 @@
1
+ import { renderHook, act, waitFor } from '@testing-library/react'
2
+ import { useIntl } from 'react-intl'
3
+ import { HttpError } from '../@errors'
4
+ import { useFetch } from './use-fetch'
5
+
6
+ jest.mock('react-intl', () => ({
7
+ useIntl: jest.fn(),
8
+ defineMessages: jest.fn(),
9
+ }))
10
+ jest.mock('@commercetools-frontend/sentry', () => ({
11
+ reportErrorToSentry: jest.fn(),
12
+ }))
13
+
14
+ describe('useFetch', () => {
15
+ const mockIntl = {
16
+ formatMessage: jest.fn((message) => message.defaultMessage),
17
+ }
18
+ const mockFetchFunction = jest.fn()
19
+ beforeEach(() => {
20
+ ;(useIntl as jest.Mock).mockReturnValue(mockIntl)
21
+ jest.clearAllMocks()
22
+ })
23
+ it('should fetch data successfully', async () => {
24
+ const data = { data: 'some data' }
25
+ mockFetchFunction.mockResolvedValueOnce(data)
26
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
27
+ expect(result.current.isLoading).toBe(true)
28
+ await waitFor(() => {
29
+ expect(result.current.isLoading).toBe(false)
30
+ })
31
+
32
+ expect(result.current.data).toEqual(data)
33
+ expect(result.current.error).toBeNull()
34
+ })
35
+ it('should handle HttpError', async () => {
36
+ const error = new HttpError(500, 'Http error')
37
+ mockFetchFunction.mockRejectedValueOnce(error)
38
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
39
+ await waitFor(() => {
40
+ expect(result.current.isLoading).toBe(false)
41
+ })
42
+ expect(result.current.data).toBeNull()
43
+ expect(result.current.error).toEqual(error)
44
+ })
45
+ it('should refetch data', async () => {
46
+ const data = { data: 'some data' }
47
+ mockFetchFunction.mockResolvedValueOnce(data)
48
+ const { result } = renderHook(() => useFetch(mockFetchFunction, {}))
49
+ await waitFor(() => {
50
+ expect(result.current.data).toEqual(data)
51
+ })
52
+ const newData = { data: 'new data' }
53
+ mockFetchFunction.mockResolvedValueOnce(newData)
54
+ act(() => {
55
+ result.current.refetch()
56
+ })
57
+ await waitFor(() => {
58
+ expect(result.current.data).toEqual(newData)
59
+ })
60
+ })
61
+ it('should handle polling', async () => {
62
+ const data = { data: 'some data' }
63
+ mockFetchFunction.mockResolvedValue(data)
64
+ const { result } = renderHook(() =>
65
+ useFetch(mockFetchFunction, { pollingInterval: 50 })
66
+ )
67
+ await waitFor(() => {
68
+ expect(result.current.data).toEqual(data)
69
+ })
70
+ const newData = { data: 'new data' }
71
+ mockFetchFunction.mockResolvedValue(newData)
72
+ await waitFor(() => {
73
+ expect(result.current.data).toEqual(newData)
74
+ })
75
+ })
76
+ })
@@ -0,0 +1,80 @@
1
+ import React from 'react'
2
+ import { useIntl } from 'react-intl'
3
+ import { reportErrorToSentry } from '@commercetools-frontend/sentry'
4
+ import { HttpError } from '../@errors'
5
+ import messages from './messages'
6
+
7
+ export const useFetch = <Data>(
8
+ fetchFunction: () => Promise<Data>,
9
+ config: {
10
+ pollingInterval?: number
11
+ shouldContinuePolling?: (data: Data) => boolean
12
+ } = {}
13
+ ) => {
14
+ const intl = useIntl()
15
+ const [data, setData] = React.useState<Data | null>(null)
16
+ const [error, setError] = React.useState<Error | null>(null)
17
+ const [isLoading, setIsLoading] = React.useState(false)
18
+ const [refetchCount, setRefetchCount] = React.useState(0)
19
+ const [lastFetchTime, setLastFetchTime] = React.useState<Date>(new Date())
20
+
21
+ const refetch = React.useCallback(() => {
22
+ setRefetchCount((count) => count + 1)
23
+ }, [])
24
+
25
+ React.useEffect(() => {
26
+ let pollingId: NodeJS.Timeout | null = null
27
+ const fetchData = async () => {
28
+ setIsLoading(true)
29
+ try {
30
+ const response = await fetchFunction()
31
+ setData(response)
32
+ setLastFetchTime(new Date())
33
+ if (
34
+ config.shouldContinuePolling &&
35
+ !config.shouldContinuePolling(response)
36
+ ) {
37
+ if (pollingId) {
38
+ clearInterval(pollingId)
39
+ pollingId = null
40
+ }
41
+ }
42
+ } catch (err) {
43
+ if (err instanceof HttpError) {
44
+ setError(err)
45
+ } else {
46
+ setError(new Error(intl.formatMessage(messages.unexpectedError)))
47
+ reportErrorToSentry(
48
+ new Error('An unexpected error occurred in the `useFetch` hook'),
49
+ {
50
+ extra: {
51
+ error: err,
52
+ },
53
+ }
54
+ )
55
+ }
56
+ } finally {
57
+ setIsLoading(false)
58
+ }
59
+ }
60
+ fetchData()
61
+ if (config.pollingInterval && config.pollingInterval > 0) {
62
+ pollingId = setInterval(fetchData, config.pollingInterval)
63
+ }
64
+ return () => {
65
+ if (pollingId) {
66
+ clearInterval(pollingId)
67
+ }
68
+ setError(null)
69
+ setIsLoading(false)
70
+ }
71
+ }, [
72
+ fetchFunction,
73
+ refetchCount,
74
+ intl,
75
+ config.pollingInterval,
76
+ config.shouldContinuePolling,
77
+ ])
78
+
79
+ return { data, error, isLoading, refetch, lastFetchTime }
80
+ }