@faststore/components 3.98.0-dev.3 → 3.98.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/dist/cjs/atoms/Link/Link.d.ts +1 -1
- package/dist/cjs/atoms/Link/Link.d.ts.map +1 -1
- package/dist/cjs/atoms/Link/Link.js.map +1 -1
- package/dist/cjs/atoms/List/List.d.ts.map +1 -1
- package/dist/cjs/atoms/List/List.js.map +1 -1
- package/dist/cjs/hooks/index.d.ts +7 -3
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.js +12 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useCSVParser.d.ts +43 -0
- package/dist/cjs/hooks/useCSVParser.d.ts.map +1 -0
- package/dist/cjs/hooks/useCSVParser.js +264 -0
- package/dist/cjs/hooks/useCSVParser.js.map +1 -0
- package/dist/cjs/hooks/useFileUpload.d.ts +26 -0
- package/dist/cjs/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/cjs/hooks/useFileUpload.js +68 -0
- package/dist/cjs/hooks/useFileUpload.js.map +1 -0
- package/dist/cjs/index.d.ts +58 -50
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +47 -32
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/molecules/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/cjs/molecules/Accordion/AccordionItem.js.map +1 -1
- package/dist/cjs/molecules/Dropzone/Dropzone.d.ts +87 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.d.ts.map +1 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.js +85 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.js.map +1 -0
- package/dist/cjs/molecules/Dropzone/index.d.ts +3 -0
- package/dist/cjs/molecules/Dropzone/index.d.ts.map +1 -0
- package/dist/cjs/molecules/Dropzone/index.js +9 -0
- package/dist/cjs/molecules/Dropzone/index.js.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js +169 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/index.d.ts +3 -0
- package/dist/cjs/molecules/FileUploadCard/index.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/index.js +9 -0
- package/dist/cjs/molecules/FileUploadCard/index.js.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js +77 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/index.d.ts +3 -0
- package/dist/cjs/molecules/FileUploadStatus/index.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/index.js +11 -0
- package/dist/cjs/molecules/FileUploadStatus/index.js.map +1 -0
- package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts +23 -1
- package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
- package/dist/cjs/molecules/SearchInputField/SearchInputField.js +9 -4
- package/dist/cjs/molecules/SearchInputField/SearchInputField.js.map +1 -1
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js +14 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +37 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +24 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +91 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts +11 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.js +18 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +86 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
- package/dist/esm/atoms/Link/Link.d.ts +1 -1
- package/dist/esm/atoms/Link/Link.d.ts.map +1 -1
- package/dist/esm/atoms/Link/Link.js.map +1 -1
- package/dist/esm/atoms/List/List.d.ts.map +1 -1
- package/dist/esm/atoms/List/List.js.map +1 -1
- package/dist/esm/hooks/index.d.ts +7 -3
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.js +6 -3
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useCSVParser.d.ts +43 -0
- package/dist/esm/hooks/useCSVParser.d.ts.map +1 -0
- package/dist/esm/hooks/useCSVParser.js +259 -0
- package/dist/esm/hooks/useCSVParser.js.map +1 -0
- package/dist/esm/hooks/useFileUpload.d.ts +26 -0
- package/dist/esm/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/esm/hooks/useFileUpload.js +64 -0
- package/dist/esm/hooks/useFileUpload.js.map +1 -0
- package/dist/esm/index.d.ts +58 -50
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +24 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/molecules/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/esm/molecules/Accordion/AccordionItem.js.map +1 -1
- package/dist/esm/molecules/Dropzone/Dropzone.d.ts +87 -0
- package/dist/esm/molecules/Dropzone/Dropzone.d.ts.map +1 -0
- package/dist/esm/molecules/Dropzone/Dropzone.js +82 -0
- package/dist/esm/molecules/Dropzone/Dropzone.js.map +1 -0
- package/dist/esm/molecules/Dropzone/index.d.ts +3 -0
- package/dist/esm/molecules/Dropzone/index.d.ts.map +1 -0
- package/dist/esm/molecules/Dropzone/index.js +2 -0
- package/dist/esm/molecules/Dropzone/index.js.map +1 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.js +166 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
- package/dist/esm/molecules/FileUploadCard/index.d.ts +3 -0
- package/dist/esm/molecules/FileUploadCard/index.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadCard/index.js +2 -0
- package/dist/esm/molecules/FileUploadCard/index.js.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js +73 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/index.d.ts +3 -0
- package/dist/esm/molecules/FileUploadStatus/index.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/index.js +2 -0
- package/dist/esm/molecules/FileUploadStatus/index.js.map +1 -0
- package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts +23 -1
- package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
- package/dist/esm/molecules/SearchInputField/SearchInputField.js +9 -4
- package/dist/esm/molecules/SearchInputField/SearchInputField.js.map +1 -1
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js +11 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +34 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +21 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +88 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.d.ts +11 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.js +6 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +80 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
- package/package.json +5 -2
- package/src/atoms/Link/Link.tsx +4 -1
- package/src/atoms/List/List.tsx +18 -16
- package/src/hooks/index.ts +11 -3
- package/src/hooks/useCSVParser.ts +367 -0
- package/src/hooks/useFileUpload.ts +88 -0
- package/src/index.ts +97 -66
- package/src/molecules/Accordion/AccordionItem.tsx +4 -3
- package/src/molecules/Dropzone/Dropzone.tsx +248 -0
- package/src/molecules/Dropzone/index.ts +2 -0
- package/src/molecules/FileUploadCard/FileUploadCard.tsx +406 -0
- package/src/molecules/FileUploadCard/index.tsx +2 -0
- package/src/molecules/FileUploadStatus/FileUploadStatus.tsx +258 -0
- package/src/molecules/FileUploadStatus/index.tsx +6 -0
- package/src/molecules/SearchInputField/SearchInputField.tsx +72 -23
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawer.tsx +58 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.tsx +72 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.tsx +43 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.tsx +323 -0
- package/src/organisms/QuickOrderDrawer/index.ts +19 -0
- package/src/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.tsx +205 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { ChangeEvent, DragEvent, HTMLAttributes } from 'react'
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button, Card, Icon, Input } from '../..'
|
|
5
|
+
import { useOnClickOutside } from '../../hooks'
|
|
6
|
+
import FileUploadStatus, {
|
|
7
|
+
FileUploadErrorType,
|
|
8
|
+
FileUploadState,
|
|
9
|
+
} from '../FileUploadStatus/FileUploadStatus'
|
|
10
|
+
|
|
11
|
+
export interface FileUploadCardProps
|
|
12
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'onDrop'> {
|
|
13
|
+
/**
|
|
14
|
+
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
|
|
15
|
+
*/
|
|
16
|
+
testId?: string
|
|
17
|
+
/**
|
|
18
|
+
* Controls whether the component is visible.
|
|
19
|
+
*/
|
|
20
|
+
isOpen: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Callback function when the component should be closed.
|
|
23
|
+
*/
|
|
24
|
+
onDismiss?: () => void
|
|
25
|
+
/**
|
|
26
|
+
* Callback function when a file is selected.
|
|
27
|
+
*/
|
|
28
|
+
onFileSelect?: (files: File[]) => void
|
|
29
|
+
/**
|
|
30
|
+
* Callback function when download template is clicked.
|
|
31
|
+
*/
|
|
32
|
+
onDownloadTemplate?: () => void
|
|
33
|
+
/**
|
|
34
|
+
* Callback function when search button is clicked after file upload.
|
|
35
|
+
*/
|
|
36
|
+
onSearch?: (file: File) => void
|
|
37
|
+
/**
|
|
38
|
+
* Accepted file types.
|
|
39
|
+
* @default '.csv'
|
|
40
|
+
*/
|
|
41
|
+
accept?: string
|
|
42
|
+
/**
|
|
43
|
+
* Allow multiple file selection.
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
multiple?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Card title (e.g. from CMS).
|
|
49
|
+
*/
|
|
50
|
+
title: string
|
|
51
|
+
/**
|
|
52
|
+
* Aria-label for the file input (e.g. from CMS).
|
|
53
|
+
*/
|
|
54
|
+
fileInputAriaLabel: string
|
|
55
|
+
/**
|
|
56
|
+
* Aria-label for the dropzone region (e.g. from CMS).
|
|
57
|
+
*/
|
|
58
|
+
dropzoneAriaLabel: string
|
|
59
|
+
/**
|
|
60
|
+
* Dropzone title text (e.g. from CMS).
|
|
61
|
+
*/
|
|
62
|
+
dropzoneTitle: string
|
|
63
|
+
/**
|
|
64
|
+
* Label for the select file button (e.g. from CMS).
|
|
65
|
+
*/
|
|
66
|
+
selectFileButtonLabel: string
|
|
67
|
+
/**
|
|
68
|
+
* Label for the download template button (e.g. from CMS).
|
|
69
|
+
*/
|
|
70
|
+
downloadTemplateButtonLabel: string
|
|
71
|
+
/**
|
|
72
|
+
* Aria-label for the remove button in FileUploadStatus (e.g. from CMS).
|
|
73
|
+
*/
|
|
74
|
+
removeButtonAriaLabel: string
|
|
75
|
+
/**
|
|
76
|
+
* Label for the search button in FileUploadStatus (e.g. from CMS).
|
|
77
|
+
*/
|
|
78
|
+
searchButtonLabel: string
|
|
79
|
+
/**
|
|
80
|
+
* Status text when uploading in FileUploadStatus (e.g. from CMS).
|
|
81
|
+
*/
|
|
82
|
+
uploadingStatusText: string
|
|
83
|
+
/**
|
|
84
|
+
* Status text when completed in FileUploadStatus (e.g. from CMS). Receives file size in bytes.
|
|
85
|
+
*/
|
|
86
|
+
getCompletedStatusText: (fileSize: number) => string
|
|
87
|
+
/**
|
|
88
|
+
* Error messages per error type for FileUploadStatus (e.g. from CMS).
|
|
89
|
+
*/
|
|
90
|
+
errorMessages: Partial<
|
|
91
|
+
Record<FileUploadErrorType, { title: string; description: string }>
|
|
92
|
+
>
|
|
93
|
+
/**
|
|
94
|
+
* Formatter for file size display.
|
|
95
|
+
*/
|
|
96
|
+
formatterFileSize?: (size: number) => string
|
|
97
|
+
/**
|
|
98
|
+
* Formatter for file name display.
|
|
99
|
+
*/
|
|
100
|
+
formatterFileName?: (name: string) => string
|
|
101
|
+
/**
|
|
102
|
+
* Indicates if the file is being uploaded.
|
|
103
|
+
*/
|
|
104
|
+
isUploading?: boolean
|
|
105
|
+
/**
|
|
106
|
+
* Indicates if there was an error during file upload.
|
|
107
|
+
*/
|
|
108
|
+
hasError?: boolean
|
|
109
|
+
/**
|
|
110
|
+
* Type of error when hasError is true.
|
|
111
|
+
*/
|
|
112
|
+
errorType?: FileUploadErrorType
|
|
113
|
+
/**
|
|
114
|
+
* Custom error message to display when hasError is true.
|
|
115
|
+
*/
|
|
116
|
+
errorMessage?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const FileUploadCard = ({
|
|
120
|
+
testId = 'fs-file-upload-card',
|
|
121
|
+
isOpen,
|
|
122
|
+
onDismiss,
|
|
123
|
+
onFileSelect,
|
|
124
|
+
onDownloadTemplate,
|
|
125
|
+
onSearch,
|
|
126
|
+
accept = '.csv',
|
|
127
|
+
multiple = false,
|
|
128
|
+
title,
|
|
129
|
+
fileInputAriaLabel,
|
|
130
|
+
dropzoneAriaLabel,
|
|
131
|
+
dropzoneTitle,
|
|
132
|
+
selectFileButtonLabel,
|
|
133
|
+
downloadTemplateButtonLabel,
|
|
134
|
+
removeButtonAriaLabel,
|
|
135
|
+
searchButtonLabel,
|
|
136
|
+
uploadingStatusText,
|
|
137
|
+
getCompletedStatusText,
|
|
138
|
+
errorMessages,
|
|
139
|
+
formatterFileSize,
|
|
140
|
+
formatterFileName,
|
|
141
|
+
isUploading = false,
|
|
142
|
+
hasError = false,
|
|
143
|
+
errorType: errorTypeProp,
|
|
144
|
+
errorMessage,
|
|
145
|
+
...otherProps
|
|
146
|
+
}: FileUploadCardProps) => {
|
|
147
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
148
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
149
|
+
const [dragActive, setDragActive] = useState(false)
|
|
150
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
151
|
+
const [uploadState, setUploadState] = useState<FileUploadState>(
|
|
152
|
+
FileUploadState.Uploading
|
|
153
|
+
)
|
|
154
|
+
const [errorType, setErrorType] = useState<FileUploadErrorType | undefined>(
|
|
155
|
+
undefined
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
useOnClickOutside(isOpen ? containerRef : undefined, () => {
|
|
159
|
+
if (isOpen) {
|
|
160
|
+
onDismiss?.()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
166
|
+
if (e.key === 'Escape' && isOpen) {
|
|
167
|
+
onDismiss?.()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
window.addEventListener('keydown', handleEscape)
|
|
172
|
+
return () => window.removeEventListener('keydown', handleEscape)
|
|
173
|
+
}, [isOpen, onDismiss])
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!selectedFile) return
|
|
177
|
+
|
|
178
|
+
if (hasError) {
|
|
179
|
+
setUploadState(FileUploadState.Error)
|
|
180
|
+
setErrorType(errorTypeProp ?? FileUploadErrorType.InvalidStructure)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isUploading) {
|
|
185
|
+
setUploadState(FileUploadState.Uploading)
|
|
186
|
+
setErrorType(undefined)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setUploadState(FileUploadState.Completed)
|
|
191
|
+
setErrorType(undefined)
|
|
192
|
+
}, [hasError, selectedFile, isUploading, errorTypeProp])
|
|
193
|
+
|
|
194
|
+
const isValidFileType = (file: File): boolean => {
|
|
195
|
+
const fileName = file.name.toLowerCase()
|
|
196
|
+
const acceptedTypes = accept
|
|
197
|
+
.split(',')
|
|
198
|
+
.map((value) => value.trim().toLowerCase())
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
|
|
201
|
+
if (acceptedTypes.length === 0) {
|
|
202
|
+
return fileName.endsWith('.csv')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return acceptedTypes.some((value) =>
|
|
206
|
+
value.startsWith('.')
|
|
207
|
+
? fileName.endsWith(value)
|
|
208
|
+
: file.type.toLowerCase() === value
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
213
|
+
const files = Array.from(e.target.files || [])
|
|
214
|
+
if (files.length > 0) {
|
|
215
|
+
const file = files[0]
|
|
216
|
+
setSelectedFile(file)
|
|
217
|
+
|
|
218
|
+
if (!isValidFileType(file)) {
|
|
219
|
+
setUploadState(FileUploadState.Error)
|
|
220
|
+
setErrorType(FileUploadErrorType.Unsupported)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setErrorType(undefined)
|
|
225
|
+
|
|
226
|
+
if (isUploading) {
|
|
227
|
+
setUploadState(FileUploadState.Uploading)
|
|
228
|
+
} else {
|
|
229
|
+
setUploadState(FileUploadState.Completed)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (onFileSelect) {
|
|
233
|
+
onFileSelect(files)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const handleDrag = (e: DragEvent<HTMLDivElement>) => {
|
|
239
|
+
if (!isOpen) return
|
|
240
|
+
e.preventDefault()
|
|
241
|
+
e.stopPropagation()
|
|
242
|
+
if (e.type === 'dragenter' || e.type === 'dragover') {
|
|
243
|
+
setDragActive(true)
|
|
244
|
+
} else if (e.type === 'dragleave') {
|
|
245
|
+
setDragActive(false)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
250
|
+
if (!isOpen) return
|
|
251
|
+
e.preventDefault()
|
|
252
|
+
e.stopPropagation()
|
|
253
|
+
setDragActive(false)
|
|
254
|
+
|
|
255
|
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
256
|
+
const files = Array.from(e.dataTransfer.files)
|
|
257
|
+
const file = files[0]
|
|
258
|
+
setSelectedFile(file)
|
|
259
|
+
|
|
260
|
+
if (!isValidFileType(file)) {
|
|
261
|
+
setUploadState(FileUploadState.Error)
|
|
262
|
+
setErrorType(FileUploadErrorType.Unsupported)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setErrorType(undefined)
|
|
267
|
+
|
|
268
|
+
if (isUploading) {
|
|
269
|
+
setUploadState(FileUploadState.Uploading)
|
|
270
|
+
} else {
|
|
271
|
+
setUploadState(FileUploadState.Completed)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (onFileSelect) {
|
|
275
|
+
onFileSelect(files)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const triggerFileInput = () => {
|
|
281
|
+
if (fileInputRef.current) {
|
|
282
|
+
fileInputRef.current.value = ''
|
|
283
|
+
}
|
|
284
|
+
fileInputRef.current?.click()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const handleDownloadTemplate = () => {
|
|
288
|
+
if (onDownloadTemplate) {
|
|
289
|
+
onDownloadTemplate()
|
|
290
|
+
} else {
|
|
291
|
+
const csvContent = 'SKU,Quantity\nAB001,2\nAB100,5\nAB999,49'
|
|
292
|
+
const blob = new Blob([csvContent], { type: 'text/csv' })
|
|
293
|
+
const url = window.URL.createObjectURL(blob)
|
|
294
|
+
const a = document.createElement('a')
|
|
295
|
+
a.href = url
|
|
296
|
+
a.download = 'template.csv'
|
|
297
|
+
a.click()
|
|
298
|
+
window.URL.revokeObjectURL(url)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const handleRemoveFile = () => {
|
|
303
|
+
setSelectedFile(null)
|
|
304
|
+
setUploadState(FileUploadState.Uploading)
|
|
305
|
+
setErrorType(undefined)
|
|
306
|
+
if (fileInputRef.current) {
|
|
307
|
+
fileInputRef.current.value = ''
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleSearch = () => {
|
|
312
|
+
if (selectedFile && onSearch) {
|
|
313
|
+
onSearch(selectedFile)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Card
|
|
319
|
+
ref={containerRef}
|
|
320
|
+
title={title}
|
|
321
|
+
data-fs-file-upload-card
|
|
322
|
+
data-fs-file-upload-card-open={isOpen}
|
|
323
|
+
data-fs-file-upload-card-has-file={selectedFile !== null}
|
|
324
|
+
testId={testId}
|
|
325
|
+
aria-hidden={!isOpen}
|
|
326
|
+
{...otherProps}
|
|
327
|
+
>
|
|
328
|
+
<Input
|
|
329
|
+
ref={fileInputRef}
|
|
330
|
+
type="file"
|
|
331
|
+
onChange={handleFileChange}
|
|
332
|
+
accept={accept}
|
|
333
|
+
multiple={multiple}
|
|
334
|
+
style={{ display: 'none' }}
|
|
335
|
+
aria-label={fileInputAriaLabel}
|
|
336
|
+
/>
|
|
337
|
+
|
|
338
|
+
{selectedFile ? (
|
|
339
|
+
<FileUploadStatus
|
|
340
|
+
file={selectedFile}
|
|
341
|
+
state={uploadState}
|
|
342
|
+
errorType={errorTypeProp ?? errorType}
|
|
343
|
+
errorMessages={errorMessages}
|
|
344
|
+
errorMessage={errorMessage}
|
|
345
|
+
onRemove={handleRemoveFile}
|
|
346
|
+
onSearch={handleSearch}
|
|
347
|
+
onDownloadTemplate={handleDownloadTemplate}
|
|
348
|
+
onSelectFile={triggerFileInput}
|
|
349
|
+
removeButtonAriaLabel={removeButtonAriaLabel}
|
|
350
|
+
searchButtonLabel={searchButtonLabel}
|
|
351
|
+
downloadTemplateButtonLabel={downloadTemplateButtonLabel}
|
|
352
|
+
selectFileButtonLabel={selectFileButtonLabel}
|
|
353
|
+
uploadingStatusText={uploadingStatusText}
|
|
354
|
+
completedStatusText={getCompletedStatusText(selectedFile.size)}
|
|
355
|
+
fileName={
|
|
356
|
+
formatterFileName
|
|
357
|
+
? formatterFileName(selectedFile.name)
|
|
358
|
+
: selectedFile.name
|
|
359
|
+
}
|
|
360
|
+
/>
|
|
361
|
+
) : (
|
|
362
|
+
<div
|
|
363
|
+
data-fs-file-upload-card-dropzone
|
|
364
|
+
data-fs-file-upload-card-dragging={dragActive}
|
|
365
|
+
onDragEnter={handleDrag}
|
|
366
|
+
onDragLeave={handleDrag}
|
|
367
|
+
onDragOver={handleDrag}
|
|
368
|
+
onDrop={handleDrop}
|
|
369
|
+
role="region"
|
|
370
|
+
aria-label={dropzoneAriaLabel}
|
|
371
|
+
>
|
|
372
|
+
<div data-fs-file-upload-card-icon>
|
|
373
|
+
<div data-fs-file-upload-card-icon-shadow />
|
|
374
|
+
<div data-fs-file-upload-card-icon-wrapper>
|
|
375
|
+
<Icon name="Paperclip" width={24} height={32} />
|
|
376
|
+
</div>
|
|
377
|
+
<div data-fs-file-upload-card-icon-badge>
|
|
378
|
+
<Icon name="Plus" width={16} height={16} />
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<p data-fs-file-upload-card-title>{dropzoneTitle}</p>
|
|
383
|
+
|
|
384
|
+
<Button
|
|
385
|
+
variant="secondary"
|
|
386
|
+
size="regular"
|
|
387
|
+
onClick={triggerFileInput}
|
|
388
|
+
data-fs-file-upload-card-select-button
|
|
389
|
+
>
|
|
390
|
+
{selectFileButtonLabel}
|
|
391
|
+
</Button>
|
|
392
|
+
|
|
393
|
+
<Button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={handleDownloadTemplate}
|
|
396
|
+
data-fs-file-upload-card-template-link
|
|
397
|
+
>
|
|
398
|
+
{downloadTemplateButtonLabel}
|
|
399
|
+
</Button>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</Card>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export default FileUploadCard
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button, Icon } from '../..'
|
|
5
|
+
|
|
6
|
+
export enum FileUploadState {
|
|
7
|
+
Uploading = 'uploading',
|
|
8
|
+
Completed = 'completed',
|
|
9
|
+
Error = 'error',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum FileUploadErrorType {
|
|
13
|
+
Unexpected = 'unexpected',
|
|
14
|
+
Unsupported = 'unsupported',
|
|
15
|
+
Unreadable = 'unreadable',
|
|
16
|
+
InvalidStructure = 'invalid-structure',
|
|
17
|
+
Empty = 'empty',
|
|
18
|
+
TooLarge = 'too-large',
|
|
19
|
+
NoProductsFound = 'no-products-found',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FileUploadStatusProps extends HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
/**
|
|
24
|
+
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
|
|
25
|
+
*/
|
|
26
|
+
testId?: string
|
|
27
|
+
/**
|
|
28
|
+
* The file being uploaded.
|
|
29
|
+
*/
|
|
30
|
+
file: File
|
|
31
|
+
/**
|
|
32
|
+
* Current upload state.
|
|
33
|
+
* @default 'uploading'
|
|
34
|
+
*/
|
|
35
|
+
state?: FileUploadState
|
|
36
|
+
/**
|
|
37
|
+
* Type of error when state is 'error'.
|
|
38
|
+
*/
|
|
39
|
+
errorType?: FileUploadErrorType
|
|
40
|
+
/**
|
|
41
|
+
* Custom error message. If provided, overrides the default error message for the errorType.
|
|
42
|
+
*/
|
|
43
|
+
errorMessage?: string
|
|
44
|
+
/**
|
|
45
|
+
* Callback when the remove/cancel button is clicked.
|
|
46
|
+
*/
|
|
47
|
+
onRemove?: () => void
|
|
48
|
+
/**
|
|
49
|
+
* Callback when the search button is clicked (only shown when state is 'completed').
|
|
50
|
+
*/
|
|
51
|
+
onSearch?: () => void
|
|
52
|
+
/**
|
|
53
|
+
* Callback when download template is clicked (only shown when state is 'error').
|
|
54
|
+
*/
|
|
55
|
+
onDownloadTemplate?: () => void
|
|
56
|
+
/**
|
|
57
|
+
* Callback when select file is clicked (only shown when state is 'error').
|
|
58
|
+
*/
|
|
59
|
+
onSelectFile?: () => void
|
|
60
|
+
/**
|
|
61
|
+
* Aria-label for the remove button (e.g. from CMS).
|
|
62
|
+
*/
|
|
63
|
+
removeButtonAriaLabel: string
|
|
64
|
+
/**
|
|
65
|
+
* Label for the search button when state is 'completed' (e.g. from CMS).
|
|
66
|
+
*/
|
|
67
|
+
searchButtonLabel: string
|
|
68
|
+
/**
|
|
69
|
+
* Label for the download template button (e.g. from CMS).
|
|
70
|
+
*/
|
|
71
|
+
downloadTemplateButtonLabel: string
|
|
72
|
+
/**
|
|
73
|
+
* Label for the select file button (e.g. from CMS).
|
|
74
|
+
*/
|
|
75
|
+
selectFileButtonLabel: string
|
|
76
|
+
/**
|
|
77
|
+
* Error messages per error type (e.g. from CMS). Required when state is Error to show messages.
|
|
78
|
+
*/
|
|
79
|
+
errorMessages: Partial<
|
|
80
|
+
Record<FileUploadErrorType, { title: string; description: string }>
|
|
81
|
+
>
|
|
82
|
+
/**
|
|
83
|
+
* Status text when state is Uploading (e.g. from CMS).
|
|
84
|
+
*/
|
|
85
|
+
uploadingStatusText: string
|
|
86
|
+
/**
|
|
87
|
+
* Status text when state is Completed (e.g. from CMS). May include file size.
|
|
88
|
+
*/
|
|
89
|
+
completedStatusText: string
|
|
90
|
+
/**
|
|
91
|
+
* Custom file name display (optional).
|
|
92
|
+
*/
|
|
93
|
+
fileName?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const FileUploadStatus = ({
|
|
97
|
+
testId = 'fs-file-upload-status',
|
|
98
|
+
file,
|
|
99
|
+
state = FileUploadState.Uploading,
|
|
100
|
+
errorType,
|
|
101
|
+
errorMessage,
|
|
102
|
+
onRemove,
|
|
103
|
+
onSearch,
|
|
104
|
+
onDownloadTemplate,
|
|
105
|
+
onSelectFile,
|
|
106
|
+
removeButtonAriaLabel,
|
|
107
|
+
searchButtonLabel,
|
|
108
|
+
downloadTemplateButtonLabel,
|
|
109
|
+
selectFileButtonLabel,
|
|
110
|
+
errorMessages,
|
|
111
|
+
uploadingStatusText,
|
|
112
|
+
completedStatusText,
|
|
113
|
+
fileName,
|
|
114
|
+
...otherProps
|
|
115
|
+
}: FileUploadStatusProps) => {
|
|
116
|
+
const getErrorMessage = (): { title: string; description: string } => {
|
|
117
|
+
if (errorMessage) {
|
|
118
|
+
return { title: errorMessage, description: '' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (errorType && errorMessages?.[errorType]) {
|
|
122
|
+
return errorMessages[errorType]!
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
errorMessages?.[FileUploadErrorType.Unexpected] ?? {
|
|
127
|
+
title: '',
|
|
128
|
+
description: '',
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const getStatusText = (): string => {
|
|
134
|
+
switch (state) {
|
|
135
|
+
case FileUploadState.Uploading:
|
|
136
|
+
return uploadingStatusText
|
|
137
|
+
case FileUploadState.Completed:
|
|
138
|
+
return completedStatusText
|
|
139
|
+
default:
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const getIcon = () => {
|
|
145
|
+
switch (state) {
|
|
146
|
+
case FileUploadState.Uploading:
|
|
147
|
+
return (
|
|
148
|
+
<div data-fs-file-upload-status-icon-loading>
|
|
149
|
+
<Icon name="CircleNotch" width={24} height={24} strokeWidth={5} />
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
case FileUploadState.Completed:
|
|
153
|
+
return (
|
|
154
|
+
<div data-fs-file-upload-status-icon-completed>
|
|
155
|
+
<Icon name="Table" width={24} height={24} strokeWidth={5} />
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
case FileUploadState.Error:
|
|
159
|
+
return (
|
|
160
|
+
<div data-fs-file-upload-status-icon-error>
|
|
161
|
+
<Icon
|
|
162
|
+
name="WarningOctagon"
|
|
163
|
+
width={24}
|
|
164
|
+
height={24}
|
|
165
|
+
strokeWidth={5}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
default:
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
data-fs-file-upload-status
|
|
177
|
+
data-fs-file-upload-status-state={state}
|
|
178
|
+
data-testid={testId}
|
|
179
|
+
{...otherProps}
|
|
180
|
+
>
|
|
181
|
+
<div
|
|
182
|
+
data-fs-file-upload-status-file-info
|
|
183
|
+
data-fs-file-upload-state={state}
|
|
184
|
+
>
|
|
185
|
+
<div data-fs-file-upload-status-icon>{getIcon()}</div>
|
|
186
|
+
|
|
187
|
+
<div data-fs-file-upload-status-details>
|
|
188
|
+
{state === FileUploadState.Error ? (
|
|
189
|
+
<>
|
|
190
|
+
<p data-fs-file-upload-status-text-error>
|
|
191
|
+
{getErrorMessage().title}
|
|
192
|
+
</p>
|
|
193
|
+
<p data-fs-file-upload-status-text-error>
|
|
194
|
+
{getErrorMessage().description}
|
|
195
|
+
</p>
|
|
196
|
+
</>
|
|
197
|
+
) : (
|
|
198
|
+
<>
|
|
199
|
+
<p data-fs-file-upload-status-filename>{fileName ?? file.name}</p>
|
|
200
|
+
<p data-fs-file-upload-status-text>{getStatusText()}</p>
|
|
201
|
+
</>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{onRemove && (
|
|
206
|
+
<Button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={onRemove}
|
|
209
|
+
data-fs-file-upload-status-remove
|
|
210
|
+
aria-label={removeButtonAriaLabel}
|
|
211
|
+
>
|
|
212
|
+
<Icon name="X" width={16} height={16} />
|
|
213
|
+
</Button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
{state === FileUploadState.Completed && onSearch && (
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="primary"
|
|
220
|
+
size="regular"
|
|
221
|
+
onClick={onSearch}
|
|
222
|
+
data-fs-file-upload-status-search-button
|
|
223
|
+
>
|
|
224
|
+
{searchButtonLabel}
|
|
225
|
+
</Button>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{state === FileUploadState.Error && (
|
|
229
|
+
<div data-fs-file-upload-status-error-actions>
|
|
230
|
+
{onDownloadTemplate && (
|
|
231
|
+
<Button
|
|
232
|
+
type="button"
|
|
233
|
+
variant="secondary"
|
|
234
|
+
size="regular"
|
|
235
|
+
onClick={onDownloadTemplate}
|
|
236
|
+
data-fs-file-upload-status-download-button
|
|
237
|
+
>
|
|
238
|
+
{downloadTemplateButtonLabel}
|
|
239
|
+
</Button>
|
|
240
|
+
)}
|
|
241
|
+
{onSelectFile && (
|
|
242
|
+
<Button
|
|
243
|
+
type="button"
|
|
244
|
+
variant="primary"
|
|
245
|
+
size="regular"
|
|
246
|
+
onClick={onSelectFile}
|
|
247
|
+
data-fs-file-upload-status-select-button
|
|
248
|
+
>
|
|
249
|
+
{selectFileButtonLabel}
|
|
250
|
+
</Button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default FileUploadStatus
|