@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,104 @@
1
+ import type {
2
+ ImportContainerPagedResponse,
3
+ ImportContainer,
4
+ ImportContainerDraft,
5
+ } from '@commercetools/importapi-sdk'
6
+ import { hasRequiredFields } from './shared'
7
+ import { type DecimalSeparator } from './file-upload'
8
+ import {
9
+ type ExtendedImportSummary,
10
+ type ImportSummary,
11
+ } from './import-summary'
12
+ import { type ImportStates } from './import-states'
13
+
14
+ export function assertImportContainerPagedResponse(
15
+ maybeImportContainerPagedResponse: unknown
16
+ ): asserts maybeImportContainerPagedResponse is ImportContainerPagedResponse {
17
+ const requiredFields = ['count', 'results']
18
+ if (hasRequiredFields(maybeImportContainerPagedResponse, requiredFields))
19
+ return
20
+ throw new Error('Invalid response')
21
+ }
22
+
23
+ export function assertImportSummary(
24
+ maybeImportSummary: unknown
25
+ ): asserts maybeImportSummary is ExtendedImportSummary {
26
+ const requiredFields = ['states', 'total']
27
+ if (hasRequiredFields(maybeImportSummary, requiredFields)) return
28
+ throw new Error('Invalid response')
29
+ }
30
+
31
+ export function assertImportContainer(
32
+ maybeImportContainerResponse: unknown
33
+ ): asserts maybeImportContainerResponse is ImportContainer {
34
+ const requiredFields = ['key', 'version']
35
+ if (hasRequiredFields(maybeImportContainerResponse, requiredFields)) return
36
+ throw new Error('Invalid response')
37
+ }
38
+
39
+ export type CancelContainerResponse = {
40
+ message: string
41
+ }
42
+
43
+ export function assertCancelContainerResponse(
44
+ maybeCancelContainerResponse: unknown
45
+ ): asserts maybeCancelContainerResponse is CancelContainerResponse {
46
+ if (
47
+ typeof maybeCancelContainerResponse === 'object' &&
48
+ maybeCancelContainerResponse !== null
49
+ ) {
50
+ const responseData = maybeCancelContainerResponse as Record<string, unknown>
51
+ if (typeof responseData.message === 'string') {
52
+ return
53
+ }
54
+ }
55
+ throw new Error('Invalid response')
56
+ }
57
+
58
+ export type ImportContainerQueryParams = {
59
+ limit?: number
60
+ offset?: number
61
+ sort?: string
62
+ tags?: string
63
+ }
64
+
65
+ // Extended types to support settings that aren't in the base SDK yet
66
+ export interface ExtendedImportContainer extends ImportContainer {
67
+ settings?: {
68
+ resourceType?: string
69
+ format?: string
70
+ decimalSeparator?: DecimalSeparator
71
+ options?: {
72
+ publishAllChanges?: boolean
73
+ unpublishAllChanges?: boolean
74
+ }
75
+ }
76
+ tags?: string[]
77
+ }
78
+
79
+ export interface ExtendedImportContainerDraft extends ImportContainerDraft {
80
+ settings?: {
81
+ resourceType?: string
82
+ format?: string
83
+ decimalSeparator?: DecimalSeparator
84
+ options?: {
85
+ publishAllChanges?: boolean
86
+ unpublishAllChanges?: boolean
87
+ }
88
+ }
89
+ tags?: string[]
90
+ }
91
+
92
+ export type ImportContainerDetails = {
93
+ importContainer: ExtendedImportContainer
94
+ importState: ImportStates
95
+ importSummary: ImportSummary
96
+ isFileUploadImport: boolean
97
+ }
98
+
99
+ export type ImportSummaries = {
100
+ results: Array<Promise<ImportContainerDetails>>
101
+ count: number
102
+ total: number
103
+ queryParams: ImportContainerQueryParams
104
+ }
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ImportOperation,
3
+ ImportOperationPagedResponse,
4
+ } from '@commercetools/importapi-sdk'
5
+ import { hasRequiredFields } from './shared'
6
+
7
+ export type ImportOperationQueryParams = {
8
+ limit?: number
9
+ offset?: number
10
+ state?: string | string[]
11
+ }
12
+
13
+ // TODO: Remove the temporarily extended ImportOperation when the SDK is updated
14
+ export interface ExtendedImportOperation extends ImportOperation {
15
+ tags?: string[]
16
+ }
17
+
18
+ // TODO: Remove the temporarily extended ImportOperationPagedResponse when the SDK is updated
19
+ export interface ExtendedImportOperationPagedResponse
20
+ extends ImportOperationPagedResponse {
21
+ results: ExtendedImportOperation[]
22
+ }
23
+
24
+ export function assertImportOperationPagedResponse(
25
+ maybeImportOperationPagedResponse: unknown
26
+ ): asserts maybeImportOperationPagedResponse is ExtendedImportOperationPagedResponse {
27
+ const requiredFields = ['count', 'total', 'results']
28
+ if (hasRequiredFields(maybeImportOperationPagedResponse, requiredFields))
29
+ return
30
+ throw new Error('Invalid response')
31
+ }
@@ -0,0 +1,9 @@
1
+ export enum ImportStates {
2
+ Processing = 'processing',
3
+ WaitForUnresolvedReferences = 'wait-for-unresolved-references',
4
+ PartiallyCompleted = 'partially-completed',
5
+ Failed = 'failed',
6
+ SuccessfullyCompleted = 'successfully-completed',
7
+ NoRunningImports = 'no-running-imports',
8
+ Canceled = 'canceled',
9
+ }
@@ -0,0 +1,22 @@
1
+ import type {
2
+ OperationStates,
3
+ ImportSummary,
4
+ } from '@commercetools/importapi-sdk'
5
+
6
+ interface ExtendedOperationStates extends OperationStates {
7
+ /**
8
+ * The number of resources in the `canceled` state.
9
+ */
10
+ readonly canceled: number
11
+ /**
12
+ * The number of resources in the `deleted` state.
13
+ */
14
+ readonly deleted: number
15
+ }
16
+
17
+ export interface ExtendedImportSummary extends ImportSummary {
18
+ readonly states: ExtendedOperationStates
19
+ }
20
+
21
+ // Re-export as ImportSummary for convenience
22
+ export { type ExtendedImportSummary as ImportSummary }
@@ -0,0 +1,9 @@
1
+ export * from './api'
2
+ export * from './basic-error-data-type'
3
+ export * from './export-operation'
4
+ export * from './file-upload'
5
+ export * from './import-container'
6
+ export * from './import-operation'
7
+ export * from './import-states'
8
+ export * from './import-summary'
9
+ export * from './shared'
@@ -0,0 +1,52 @@
1
+ export function hasOwnProperty<X extends {}, Y extends PropertyKey>(
2
+ obj: X,
3
+ prop: Y
4
+ ): obj is X & Record<Y, unknown> {
5
+ return typeof obj === 'object' && obj !== null && obj.hasOwnProperty(prop)
6
+ }
7
+
8
+ export function hasRequiredFields(
9
+ maybeValidObject: unknown,
10
+ requiredFields: Array<string>
11
+ ): boolean {
12
+ return (
13
+ typeof maybeValidObject === 'object' &&
14
+ maybeValidObject !== null &&
15
+ requiredFields.every((property) =>
16
+ hasOwnProperty(maybeValidObject, property)
17
+ )
18
+ )
19
+ }
20
+
21
+ export function getMissingRequiredFields(
22
+ maybeValidObject: unknown,
23
+ requiredFields: Array<string>
24
+ ): string[] | false {
25
+ return (
26
+ typeof maybeValidObject === 'object' &&
27
+ maybeValidObject !== null &&
28
+ requiredFields.filter((property) => !(property in maybeValidObject))
29
+ )
30
+ }
31
+
32
+ export function isError(maybeError: unknown): maybeError is Error {
33
+ if (maybeError instanceof Error) return true
34
+ return false
35
+ }
36
+
37
+ export function isAbortError(error: unknown): error is DOMException {
38
+ return error instanceof DOMException && error.name === 'AbortError'
39
+ }
40
+
41
+ export function isResourceType<T extends string>(
42
+ maybeResourceType: unknown
43
+ ): maybeResourceType is T {
44
+ return typeof maybeResourceType === 'string'
45
+ }
46
+
47
+ export function assertResourceType<T extends string>(
48
+ maybeResourceType: unknown
49
+ ): asserts maybeResourceType is T {
50
+ if (typeof maybeResourceType === 'string') return
51
+ throw new Error(`Invalid value: ${maybeResourceType}`)
52
+ }
@@ -0,0 +1,126 @@
1
+ import { mapUploadFileErrorsResponseToUploadFileErrorRows } from './error-mapping'
2
+
3
+ type RowErrorsResponse = {
4
+ row?: number
5
+ index?: number
6
+ errors: Array<{ field: string; code: string; message: string }>
7
+ }
8
+
9
+ describe('mapUploadFileErrorsResponseToUploadFileErrorRows', () => {
10
+ it('should return an empty array if uploadFileErrorsResponse is undefined', () => {
11
+ const result = mapUploadFileErrorsResponseToUploadFileErrorRows(undefined)
12
+ expect(result).toEqual([])
13
+ })
14
+ it('should map the response errors correctly', () => {
15
+ const sampleErrorsResponse: Array<RowErrorsResponse> = [
16
+ {
17
+ row: 1,
18
+ errors: [
19
+ {
20
+ code: 'InvalidField',
21
+ message: '"name.de2" is not allowed',
22
+ field: 'name.de2',
23
+ },
24
+ {
25
+ code: 'InvalidField',
26
+ message: '"name.enn" is not allowed',
27
+ field: 'name.enn',
28
+ },
29
+ ],
30
+ },
31
+ {
32
+ row: 2,
33
+ errors: [
34
+ {
35
+ code: 'InvalidField',
36
+ message: '"slug.RU" is not allowed',
37
+ field: 'slug.RU',
38
+ },
39
+ ],
40
+ },
41
+ ]
42
+ const result =
43
+ mapUploadFileErrorsResponseToUploadFileErrorRows(sampleErrorsResponse)
44
+ expect(result).toMatchInlineSnapshot(`
45
+ [
46
+ {
47
+ "code": "InvalidField",
48
+ "field": "name.de2",
49
+ "id": "1",
50
+ "row": 1,
51
+ "validationMessage": ""name.de2" is not allowed",
52
+ },
53
+ {
54
+ "code": "InvalidField",
55
+ "field": "name.enn",
56
+ "id": "2",
57
+ "row": 1,
58
+ "validationMessage": ""name.enn" is not allowed",
59
+ },
60
+ {
61
+ "code": "InvalidField",
62
+ "field": "slug.RU",
63
+ "id": "3",
64
+ "row": 2,
65
+ "validationMessage": ""slug.RU" is not allowed",
66
+ },
67
+ ]
68
+ `)
69
+ })
70
+ it('should map the response errors correctly for json payload', () => {
71
+ const sampleErrorsResponse: Array<RowErrorsResponse> = [
72
+ {
73
+ index: 0,
74
+ errors: [
75
+ {
76
+ code: 'InvalidField',
77
+ message: '"name.de2" is not allowed',
78
+ field: 'name.de2',
79
+ },
80
+ {
81
+ code: 'InvalidField',
82
+ message: '"name.enn" is not allowed',
83
+ field: 'name.enn',
84
+ },
85
+ ],
86
+ },
87
+ {
88
+ index: 1,
89
+ errors: [
90
+ {
91
+ code: 'InvalidField',
92
+ message: '"slug.RU" is not allowed',
93
+ field: 'slug.RU',
94
+ },
95
+ ],
96
+ },
97
+ ]
98
+ const result =
99
+ mapUploadFileErrorsResponseToUploadFileErrorRows(sampleErrorsResponse)
100
+ expect(result).toMatchInlineSnapshot(`
101
+ [
102
+ {
103
+ "code": "InvalidField",
104
+ "field": "name.de2",
105
+ "id": "1",
106
+ "index": 0,
107
+ "validationMessage": ""name.de2" is not allowed",
108
+ },
109
+ {
110
+ "code": "InvalidField",
111
+ "field": "name.enn",
112
+ "id": "2",
113
+ "index": 0,
114
+ "validationMessage": ""name.enn" is not allowed",
115
+ },
116
+ {
117
+ "code": "InvalidField",
118
+ "field": "slug.RU",
119
+ "id": "3",
120
+ "index": 1,
121
+ "validationMessage": ""slug.RU" is not allowed",
122
+ },
123
+ ]
124
+ `)
125
+ })
126
+ })
@@ -0,0 +1,39 @@
1
+ export function getFileUploadErrorsCount(
2
+ errors?: Array<{ errors: Array<unknown> }>
3
+ ): number {
4
+ if (!errors || !Array.isArray(errors)) return 0
5
+ return errors.reduce((acc, curr) => (acc += curr.errors.length), 0)
6
+ }
7
+
8
+ export function mapUploadFileErrorsResponseToUploadFileErrorRows(
9
+ uploadFileErrorsResponse?: Array<{
10
+ row?: number
11
+ index?: number
12
+ errors: Array<{ field: string; code: string; message: string }>
13
+ }>
14
+ ): Array<{
15
+ id: string
16
+ row?: number
17
+ index?: number
18
+ field: string
19
+ code: string
20
+ validationMessage: string
21
+ }> {
22
+ if (!uploadFileErrorsResponse || !Array.isArray(uploadFileErrorsResponse))
23
+ return []
24
+ let idCounter = 1
25
+ return uploadFileErrorsResponse.flatMap((rowErrorsResponse) =>
26
+ rowErrorsResponse.errors.map((rowError) => ({
27
+ id: String(idCounter++),
28
+ ...(rowErrorsResponse.row !== undefined
29
+ ? { row: rowErrorsResponse.row }
30
+ : {}),
31
+ ...(rowErrorsResponse.index !== undefined
32
+ ? { index: rowErrorsResponse.index }
33
+ : {}),
34
+ field: rowError.field,
35
+ code: rowError.code,
36
+ validationMessage: rowError.message,
37
+ }))
38
+ )
39
+ }
@@ -0,0 +1,151 @@
1
+ import { act } from '@testing-library/react'
2
+ import { validateDelimiter, getRowCount } from './file-upload'
3
+ import { COLUMN_DELIMITERS } from '../@constants'
4
+
5
+ const createFakeFile = (content: string) => {
6
+ const blob = new Blob([content], { type: 'text/csv' })
7
+ return new File([blob], 'test.csv', {
8
+ type: 'text/csv',
9
+ })
10
+ }
11
+
12
+ describe('validateDelimiter', () => {
13
+ it('returns true for an allowed delimiter ","', async () => {
14
+ const fileContent =
15
+ 'key,externalId,orderHint,name.de\nsample-category-key,sample-category-external-id,0.5,sample-category-name-de'
16
+ const file = createFakeFile(fileContent)
17
+ let result: boolean = false
18
+ await act(async () => {
19
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
20
+ })
21
+ expect(result).toBe(true)
22
+ })
23
+ it('returns true for an allowed delimiter ";"', async () => {
24
+ const fileContent =
25
+ 'key;externalId;orderHint;name.de\nsample-category-key;sample-category-external-id;0.5;sample-category-name-de'
26
+ const file = createFakeFile(fileContent)
27
+ let result: boolean = false
28
+ await act(async () => {
29
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
30
+ })
31
+ expect(result).toBe(true)
32
+ })
33
+ it('returns true for an allowed delimiter "|"', async () => {
34
+ const fileContent =
35
+ 'key|externalId|orderHint|name.de\nsample-category-key|sample-category-external-id|0.5|sample-category-name-de'
36
+ const file = createFakeFile(fileContent)
37
+ let result: boolean = false
38
+ await act(async () => {
39
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
40
+ })
41
+ expect(result).toBe(true)
42
+ })
43
+ it('returns true for an allowed delimiter "\\t"', async () => {
44
+ const fileContent =
45
+ 'key\texternalId\torderHint\tname.de\nsample-category-key\tsample-category-external-id\t0.5\tsample-category-name-de'
46
+ const file = createFakeFile(fileContent)
47
+ let result: boolean = false
48
+ await act(async () => {
49
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
50
+ })
51
+ expect(result).toBe(true)
52
+ })
53
+ it('returns false for a non-allowed delimiter "."', async () => {
54
+ const fileContent =
55
+ 'key.externalId.orderHint.name.de\nsample-category-key.sample-category-external-id.0.5.sample-category-name-de'
56
+ const file = createFakeFile(fileContent)
57
+ let result: boolean = false
58
+ await act(async () => {
59
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
60
+ })
61
+ expect(result).toBe(false)
62
+ })
63
+ it('returns false for a non-allowed delimiter " "', async () => {
64
+ const fileContent =
65
+ 'key externalId orderHint name.de\nsample-category-key sample-category-external-id 0.5 sample-category-name-de'
66
+ const file = createFakeFile(fileContent)
67
+ let result: boolean = false
68
+ await act(async () => {
69
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
70
+ })
71
+ expect(result).toBe(false)
72
+ })
73
+ it('returns false for a non-allowed delimiter "%"', async () => {
74
+ const fileContent =
75
+ 'key%externalId%orderHint%name.de\nsample-category-key%sample-category-external-id%0.5%sample-category-name-de'
76
+ const file = createFakeFile(fileContent)
77
+ let result: boolean = false
78
+ await act(async () => {
79
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
80
+ })
81
+ expect(result).toBe(false)
82
+ })
83
+ it('returns true for a single-column file', async () => {
84
+ const fileContent = 'key'
85
+ const file = createFakeFile(fileContent)
86
+ let result: boolean = false
87
+ await act(async () => {
88
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
89
+ })
90
+ expect(result).toBe(true)
91
+ })
92
+ it('returns false for a file with undetectable delimiter', async () => {
93
+ const fileContent =
94
+ 'key.externalId.orderHint.name\nvalue1.value2.value3.value4'
95
+ const file = createFakeFile(fileContent)
96
+ let result: boolean = false
97
+ await act(async () => {
98
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
99
+ })
100
+ expect(result).toBe(false)
101
+ })
102
+ it('returns false for a file with a non allowed column delimiter', async () => {
103
+ const fileContent =
104
+ 'key externalId orderHint name\nvalue1 value2 value3 value4'
105
+ const file = createFakeFile(fileContent)
106
+ let result: boolean = false
107
+ await act(async () => {
108
+ result = await validateDelimiter(file, COLUMN_DELIMITERS)
109
+ })
110
+ expect(result).toBe(false)
111
+ })
112
+ })
113
+
114
+ describe('getRowCount', () => {
115
+ it('returns count > 1 for a file with a header and at least one data row', async () => {
116
+ const fileContent = 'header1,header2\nsample-cell1,sample-cell2'
117
+ const file = createFakeFile(fileContent)
118
+ let rowCount: number
119
+ await act(async () => {
120
+ rowCount = await getRowCount(file)
121
+ })
122
+ expect(rowCount!).toBeGreaterThanOrEqual(1)
123
+ })
124
+ it('returns count of 0 for a file with only a header', async () => {
125
+ const fileContent = 'header1,header2'
126
+ const file = createFakeFile(fileContent)
127
+ let rowCount: number
128
+ await act(async () => {
129
+ rowCount = await getRowCount(file)
130
+ })
131
+ expect(rowCount!).toBe(0)
132
+ })
133
+ it('returns count of 0 for a file with only a header and empty line', async () => {
134
+ const fileContent = 'header1,header2\n'
135
+ const file = createFakeFile(fileContent)
136
+ let rowCount: number
137
+ await act(async () => {
138
+ rowCount = await getRowCount(file)
139
+ })
140
+ expect(rowCount!).toBe(0)
141
+ })
142
+ it('returns count of 0 for an empty file', async () => {
143
+ const fileContent = ''
144
+ const file = createFakeFile(fileContent)
145
+ let rowCount: number
146
+ await act(async () => {
147
+ rowCount = await getRowCount(file)
148
+ })
149
+ expect(rowCount!).toBe(0)
150
+ })
151
+ })
@@ -0,0 +1,150 @@
1
+ import Papa from 'papaparse'
2
+
3
+ /**
4
+ * Convert megabytes to bytes
5
+ */
6
+ export const toBytes = (megabytes: number): number => megabytes * 1024 * 1024
7
+
8
+ /**
9
+ * Returns the number of rows in a CSV file excluding the header
10
+ * @param file The CSV file to process
11
+ * @returns A promise that resolves to the number of rows
12
+ */
13
+ export const getRowCount = (file: File): Promise<number> => {
14
+ return new Promise((resolve) => {
15
+ let lineCount = 0
16
+ Papa.parse(file, {
17
+ step: ({ data }: { data: unknown[] }) => {
18
+ // empty lines at the end of the file should not be counted
19
+ if (Array.isArray(data) && data.find(Boolean)) lineCount++
20
+ },
21
+ complete: () => {
22
+ // Subtract 1 for the header row
23
+ // We use Math.max to make sure the count is not less than 0, this is needed for empty files
24
+ resolve(Math.max(0, lineCount - 1))
25
+ },
26
+ })
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Check if a CSV file has a single key column
32
+ * @param file The CSV file to check
33
+ * @returns A promise that resolves to true if the file has only one column named 'key'
34
+ */
35
+ export const hasSingleKeyColumn = (file: File): Promise<boolean> => {
36
+ return new Promise((resolve) => {
37
+ Papa.parse(file, {
38
+ preview: 1,
39
+ complete: (result) => {
40
+ const headerRow = result.data?.[0]
41
+ const hasSingleColumn =
42
+ Array.isArray(headerRow) && headerRow.length === 1
43
+ const isKeyColumn =
44
+ hasSingleColumn && headerRow[0].toLowerCase() === 'key'
45
+ resolve(isKeyColumn)
46
+ },
47
+ })
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Validate delimiter in a CSV file
53
+ * @param file The CSV file to validate
54
+ * @param allowedDelimiters Array of allowed delimiters
55
+ * @returns A promise that resolves to false if delimiter is invalid
56
+ */
57
+ export const validateDelimiter = (
58
+ file: File,
59
+ allowedDelimiters: string[]
60
+ ): Promise<boolean> => {
61
+ // Delimiters not included in this array may be treated as part of a column content
62
+ // potentially causing the file to be parsed as a single-column CSV and pass this validation
63
+ const NON_ALLOWED_COLUMN_DELIMITERS = ['%', '.', ' ']
64
+ const DELIMITERS_TO_GUESS = [
65
+ ...allowedDelimiters,
66
+ ...NON_ALLOWED_COLUMN_DELIMITERS,
67
+ ]
68
+
69
+ return new Promise((resolve) => {
70
+ Papa.parse<unknown[]>(file, {
71
+ delimitersToGuess: DELIMITERS_TO_GUESS,
72
+ preview: 10,
73
+ complete: (result) => {
74
+ const headerRow = result.data?.[0]
75
+ const isOnlyOneColumn =
76
+ Array.isArray(headerRow) && headerRow.length === 1
77
+
78
+ if (isOnlyOneColumn) {
79
+ resolve(true)
80
+ return
81
+ }
82
+
83
+ const detectedDelimiter = result.meta.delimiter
84
+ let isValid = false
85
+
86
+ if (
87
+ detectedDelimiter !== null &&
88
+ allowedDelimiters.includes(detectedDelimiter)
89
+ ) {
90
+ const isUndetectableDelimiter = result.errors.some(
91
+ (error) => error.code === 'UndetectableDelimiter'
92
+ )
93
+ if (!isUndetectableDelimiter) {
94
+ isValid = true
95
+ }
96
+ }
97
+ resolve(isValid)
98
+ },
99
+ })
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Returns columns from the provided `columns` excluding those specified in the `ignoredColumns`
105
+ */
106
+ export const getValidatedColumns = (
107
+ columns: string[],
108
+ ignoredColumns: string[]
109
+ ): string[] => {
110
+ return columns.filter((column) => !ignoredColumns.includes(column))
111
+ }
112
+
113
+ /**
114
+ * Count items in a JSON file
115
+ * @param file The JSON file to process
116
+ * @returns Object with isValid flag and optional itemsCount
117
+ */
118
+ export const countJsonFileItems = async (
119
+ file: File
120
+ ): Promise<{ isValid: false } | { isValid: true; itemsCount: number }> => {
121
+ const jsonContent = await file.text()
122
+ try {
123
+ const content = JSON.parse(jsonContent)
124
+ const isValid = Array.isArray(content)
125
+ if (isValid) {
126
+ return { isValid, itemsCount: content.length }
127
+ }
128
+ return { isValid }
129
+ } catch {
130
+ return { isValid: false }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Map file upload errors to upload file error rows with unique IDs
136
+ * @param uploadFileErrors Array of file upload errors
137
+ * @returns Array of upload file errors with unique id field
138
+ */
139
+ export const mapFileUploadErrorsToUploadFileErrorRows = <
140
+ T extends Record<string, unknown>
141
+ >(
142
+ uploadFileErrors: T[]
143
+ ): Array<T & { id: string }> => {
144
+ let idCounter = 1
145
+ return uploadFileErrors.map((uploadFileError) => ({
146
+ // DataTable component requires unique `id` field
147
+ ...uploadFileError,
148
+ id: String(idCounter++),
149
+ }))
150
+ }