@commercetools-frontend-extensions/operations 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +169 -0
- package/babel.config.js +6 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.d.ts +2 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +2469 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.js +7 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +2461 -0
- package/dist/commercetools-frontend-extensions-operations.esm.js +2316 -0
- package/dist/declarations/src/@api/export-operations.d.ts +5 -0
- package/dist/declarations/src/@api/fetcher.d.ts +17 -0
- package/dist/declarations/src/@api/file-upload.d.ts +3 -0
- package/dist/declarations/src/@api/import-containers.d.ts +35 -0
- package/dist/declarations/src/@api/import-operations.d.ts +6 -0
- package/dist/declarations/src/@api/index.d.ts +8 -0
- package/dist/declarations/src/@api/process-file.d.ts +3 -0
- package/dist/declarations/src/@api/test-fixtures.d.ts +272 -0
- package/dist/declarations/src/@api/urls.d.ts +44 -0
- package/dist/declarations/src/@components/file-drop-area/active-drag-drop-area.d.ts +10 -0
- package/dist/declarations/src/@components/file-drop-area/disabled-drop-area.d.ts +5 -0
- package/dist/declarations/src/@components/file-drop-area/drop-area-wrapper.d.ts +11 -0
- package/dist/declarations/src/@components/file-drop-area/enabled-drop-area.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/file-drop-area.d.ts +14 -0
- package/dist/declarations/src/@components/file-drop-area/file-dropped-area.d.ts +6 -0
- package/dist/declarations/src/@components/file-drop-area/index.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/styles.d.ts +9 -0
- package/dist/declarations/src/@components/icons/file-icon.d.ts +2 -0
- package/dist/declarations/src/@components/icons/index.d.ts +2 -0
- package/dist/declarations/src/@components/icons/lock-icon.d.ts +2 -0
- package/dist/declarations/src/@components/index.d.ts +6 -0
- package/dist/declarations/src/@components/info-box/index.d.ts +1 -0
- package/dist/declarations/src/@components/info-box/info-box.d.ts +7 -0
- package/dist/declarations/src/@components/upload-separator/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-separator/upload-separator.d.ts +12 -0
- package/dist/declarations/src/@components/upload-settings/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-settings/upload-settings.d.ts +11 -0
- package/dist/declarations/src/@components/uploading-modal/index.d.ts +1 -0
- package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +12 -0
- package/dist/declarations/src/@constants/delimiters.d.ts +8 -0
- package/dist/declarations/src/@constants/import-tags.d.ts +7 -0
- package/dist/declarations/src/@constants/index.d.ts +4 -0
- package/dist/declarations/src/@constants/resource-links.d.ts +10 -0
- package/dist/declarations/src/@constants/upload-limits.d.ts +10 -0
- package/dist/declarations/src/@errors/http-error.d.ts +6 -0
- package/dist/declarations/src/@errors/index.d.ts +8 -0
- package/dist/declarations/src/@errors/invalid-response-error.d.ts +3 -0
- package/dist/declarations/src/@errors/no-resources-to-export-error.d.ts +3 -0
- package/dist/declarations/src/@errors/project-key-not-available-error.d.ts +3 -0
- package/dist/declarations/src/@errors/query-predicate-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-column-error.d.ts +3 -0
- package/dist/declarations/src/@errors/unexpected-operation-state-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-resource-type-error.d.ts +3 -0
- package/dist/declarations/src/@hooks/index.d.ts +5 -0
- package/dist/declarations/src/@hooks/use-fetch-export-operations.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-container-details.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-operations.d.ts +16 -0
- package/dist/declarations/src/@hooks/use-fetch-import-summaries.d.ts +20 -0
- package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +18 -0
- package/dist/declarations/src/@types/api.d.ts +13 -0
- package/dist/declarations/src/@types/basic-error-data-type.d.ts +5 -0
- package/dist/declarations/src/@types/export-operation.d.ts +95 -0
- package/dist/declarations/src/@types/file-upload.d.ts +63 -0
- package/dist/declarations/src/@types/import-container.d.ts +53 -0
- package/dist/declarations/src/@types/import-operation.d.ts +13 -0
- package/dist/declarations/src/@types/import-states.d.ts +9 -0
- package/dist/declarations/src/@types/import-summary.d.ts +15 -0
- package/dist/declarations/src/@types/index.d.ts +9 -0
- package/dist/declarations/src/@types/shared.d.ts +7 -0
- package/dist/declarations/src/@utils/error-mapping.d.ts +19 -0
- package/dist/declarations/src/@utils/file-upload.d.ts +46 -0
- package/dist/declarations/src/@utils/form.d.ts +1 -0
- package/dist/declarations/src/@utils/format.d.ts +5 -0
- package/dist/declarations/src/@utils/import-container.d.ts +8 -0
- package/dist/declarations/src/@utils/index.d.ts +6 -0
- package/dist/declarations/src/@utils/url.d.ts +6 -0
- package/dist/declarations/src/index.d.ts +26 -0
- package/index.js +1 -0
- package/jest.test.config.js +11 -0
- package/package.json +63 -0
- package/src/@api/export-operations.ts +26 -0
- package/src/@api/fetcher.spec.ts +51 -0
- package/src/@api/fetcher.ts +127 -0
- package/src/@api/file-upload.spec.ts +83 -0
- package/src/@api/file-upload.ts +46 -0
- package/src/@api/import-containers.ts +256 -0
- package/src/@api/import-operations.ts +33 -0
- package/src/@api/index.ts +8 -0
- package/src/@api/process-file.spec.ts +74 -0
- package/src/@api/process-file.ts +53 -0
- package/src/@api/test-fixtures.ts +772 -0
- package/src/@api/urls.ts +118 -0
- package/src/@components/file-drop-area/active-drag-drop-area.tsx +33 -0
- package/src/@components/file-drop-area/disabled-drop-area.tsx +17 -0
- package/src/@components/file-drop-area/drop-area-wrapper.tsx +38 -0
- package/src/@components/file-drop-area/enabled-drop-area.tsx +27 -0
- package/src/@components/file-drop-area/file-drop-area.tsx +74 -0
- package/src/@components/file-drop-area/file-dropped-area.tsx +29 -0
- package/src/@components/file-drop-area/index.ts +7 -0
- package/src/@components/file-drop-area/styles.ts +67 -0
- package/src/@components/icons/file-icon.tsx +30 -0
- package/src/@components/icons/index.ts +2 -0
- package/src/@components/icons/lock-icon.tsx +34 -0
- package/src/@components/index.ts +6 -0
- package/src/@components/info-box/index.ts +1 -0
- package/src/@components/info-box/info-box.tsx +23 -0
- package/src/@components/upload-separator/index.ts +1 -0
- package/src/@components/upload-separator/upload-separator.tsx +61 -0
- package/src/@components/upload-settings/index.ts +1 -0
- package/src/@components/upload-settings/upload-settings.tsx +36 -0
- package/src/@components/uploading-modal/index.ts +1 -0
- package/src/@components/uploading-modal/uploading-modal.tsx +64 -0
- package/src/@constants/delimiters.ts +14 -0
- package/src/@constants/import-tags.ts +9 -0
- package/src/@constants/index.ts +4 -0
- package/src/@constants/resource-links.ts +61 -0
- package/src/@constants/upload-limits.ts +11 -0
- package/src/@errors/http-error.ts +17 -0
- package/src/@errors/index.ts +8 -0
- package/src/@errors/invalid-response-error.ts +6 -0
- package/src/@errors/no-resources-to-export-error.ts +6 -0
- package/src/@errors/project-key-not-available-error.ts +6 -0
- package/src/@errors/query-predicate-error.ts +10 -0
- package/src/@errors/unexpected-column-error.ts +6 -0
- package/src/@errors/unexpected-operation-state-error.ts +8 -0
- package/src/@errors/unexpected-resource-type-error.ts +6 -0
- package/src/@hooks/index.ts +5 -0
- package/src/@hooks/messages.ts +11 -0
- package/src/@hooks/use-fetch-export-operations.ts +34 -0
- package/src/@hooks/use-fetch-import-container-details.ts +31 -0
- package/src/@hooks/use-fetch-import-operations.ts +42 -0
- package/src/@hooks/use-fetch-import-summaries.ts +47 -0
- package/src/@hooks/use-fetch.spec.ts +76 -0
- package/src/@hooks/use-fetch.ts +80 -0
- package/src/@hooks/use-import-container-upload.spec.ts +294 -0
- package/src/@hooks/use-import-container-upload.ts +126 -0
- package/src/@types/api.ts +14 -0
- package/src/@types/basic-error-data-type.ts +5 -0
- package/src/@types/export-operation.ts +144 -0
- package/src/@types/file-upload.ts +81 -0
- package/src/@types/import-container.ts +104 -0
- package/src/@types/import-operation.ts +31 -0
- package/src/@types/import-states.ts +9 -0
- package/src/@types/import-summary.ts +22 -0
- package/src/@types/index.ts +9 -0
- package/src/@types/shared.ts +52 -0
- package/src/@utils/error-mapping.spec.ts +126 -0
- package/src/@utils/error-mapping.ts +39 -0
- package/src/@utils/file-upload.spec.ts +151 -0
- package/src/@utils/file-upload.ts +150 -0
- package/src/@utils/form.ts +20 -0
- package/src/@utils/format.spec.ts +62 -0
- package/src/@utils/format.ts +53 -0
- package/src/@utils/import-container.spec.ts +26 -0
- package/src/@utils/import-container.ts +34 -0
- package/src/@utils/index.ts +6 -0
- package/src/@utils/url.spec.ts +75 -0
- package/src/@utils/url.ts +18 -0
- package/src/index.ts +27 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,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
|
+
}
|