├── .babelrc.json
├── .gitignore
├── .prettierrc
├── .storybook
├── index.css
├── main.ts
└── preview.ts
├── .yarnrc.yml
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── README.md
├── index.html
├── modal-test-js.html
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── reportWebVitals.js
│ └── setupTests.js
└── yarn.lock
├── package-js.json
├── package.json
├── rollup.config-js.js
├── rollup.config.js
├── src
├── components
│ └── CSVImporter
│ │ ├── CSVImporter.stories.tsx
│ │ ├── index.tsx
│ │ └── style
│ │ └── csv-importer.css
├── i18n
│ ├── de.ts
│ ├── es.ts
│ ├── fr.ts
│ ├── i18n.ts
│ └── it.ts
├── importer
│ ├── components
│ │ ├── Box
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Box.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Checkbox
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Checkbox.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Errors
│ │ │ ├── index.tsx
│ │ │ └── style
│ │ │ │ └── Errors.module.scss
│ │ ├── Input
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ ├── Input.module.scss
│ │ │ │ └── mixins.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Portal
│ │ │ ├── index.tsx
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Stepper
│ │ │ ├── hooks
│ │ │ │ └── useStepper.ts
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Stepper.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Table
│ │ │ ├── index.tsx
│ │ │ ├── storyData.ts
│ │ │ ├── style
│ │ │ │ └── Default.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── ToggleFilter
│ │ │ ├── ToggleFilter.stories.tsx
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── ToggleFilter.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── Tooltip
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Tooltip.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ └── UploaderWrapper
│ │ │ ├── UploaderWrapper.tsx
│ │ │ ├── style
│ │ │ └── uppy.overrides.scss
│ │ │ └── types
│ │ │ └── index.ts
│ ├── features
│ │ ├── complete
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Complete.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── main
│ │ │ ├── hooks
│ │ │ │ ├── useMutableLocalStorage.ts
│ │ │ │ └── useStepNavigation.ts
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── Main.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── map-columns
│ │ │ ├── components
│ │ │ │ └── DropDownFields.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useMapColumnsTable.tsx
│ │ │ │ └── useNameChange.ts
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── MapColumns.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ ├── row-selection
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ │ └── RowSelection.module.scss
│ │ │ └── types
│ │ │ │ └── index.ts
│ │ └── uploader
│ │ │ ├── hooks
│ │ │ └── useTemplateTable.tsx
│ │ │ ├── index.tsx
│ │ │ ├── style
│ │ │ └── Uploader.module.scss
│ │ │ └── types
│ │ │ └── index.ts
│ ├── hooks
│ │ ├── useClickOutside.ts
│ │ ├── useCustomStyles.ts
│ │ ├── useDelayLoader.ts
│ │ ├── useEventListener.ts
│ │ ├── useIsomorphicLayoutEffect.ts
│ │ ├── useRect.ts
│ │ └── useWindowSize.ts
│ ├── providers
│ │ ├── Theme.tsx
│ │ ├── index.tsx
│ │ └── types
│ │ │ └── index.ts
│ ├── settings
│ │ ├── chakra
│ │ │ ├── components
│ │ │ │ ├── alert.ts
│ │ │ │ ├── button.ts
│ │ │ │ └── index.ts
│ │ │ ├── foundations
│ │ │ │ ├── blur.ts
│ │ │ │ ├── borders.ts
│ │ │ │ ├── breakpoints.ts
│ │ │ │ ├── colors.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── radius.ts
│ │ │ │ ├── shadows.ts
│ │ │ │ ├── sizes.ts
│ │ │ │ ├── spacing.ts
│ │ │ │ ├── transition.ts
│ │ │ │ ├── typography.ts
│ │ │ │ └── z-index.ts
│ │ │ ├── index.ts
│ │ │ ├── semantic-tokens.ts
│ │ │ ├── styles.ts
│ │ │ ├── theme.types.ts
│ │ │ └── utils
│ │ │ │ ├── is-chakra-theme.ts
│ │ │ │ └── run-if-fn.ts
│ │ └── theme
│ │ │ ├── colors.ts
│ │ │ ├── index.ts
│ │ │ └── sizes.ts
│ ├── stores
│ │ └── theme.ts
│ ├── style
│ │ ├── design-system
│ │ │ └── colors.scss
│ │ ├── fonts.scss
│ │ ├── index.scss
│ │ ├── mixins.scss
│ │ ├── themes
│ │ │ ├── common.scss
│ │ │ ├── dark.scss
│ │ │ └── light.scss
│ │ └── vars.scss
│ ├── types
│ │ └── index.ts
│ └── utils
│ │ ├── classes.ts
│ │ ├── debounce.ts
│ │ ├── getStringLengthOfChildren.ts
│ │ ├── stringSimilarity.ts
│ │ ├── template.ts
│ │ └── utils.ts
├── index.ts
├── js.tsx
├── settings
│ └── defaults.ts
├── styles.d.ts
└── types
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourceType": "unambiguous",
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "targets": {
8 | "chrome": 100
9 | }
10 | }
11 | ],
12 | "@babel/preset-typescript",
13 | "@babel/preset-react"
14 | ],
15 | "plugins": []
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.iml
3 | *.swp
4 | *.swo
5 | *.dll
6 | *.so
7 | *.dylib
8 | *.test
9 | *.out
10 | *.log
11 | *.env
12 | *.zip
13 |
14 | # debug
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 | node_modules/
20 | vendor/
21 | build/
22 | .yarn/
23 | .pnp*
24 |
25 | .DS_Store
26 | .idea
27 | .vscode
28 |
29 | /coverage
30 | /.next/
31 | /out/
32 |
33 | # misc
34 | *.pem
35 |
36 | # local env files
37 | .env*.local
38 |
39 | # vercel
40 | .vercel
41 |
42 | # typescript
43 | *.tsbuildinfo
44 | next-env.d.ts
45 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": false,
4 | "tabWidth": 2,
5 | "jsxSingleQuote": false,
6 | "bracketSameLine": true,
7 | "printWidth": 150,
8 | "endOfLine": "auto",
9 | "importOrder": [
10 | "^[^/.]+$",
11 | "^@.+$",
12 | "^\\.\\./(.*)/[A-Z]((?!\\.)(?!types.).)+$",
13 | "^\\.\\./(.*)/((?!\\.)(?!types.).)+$",
14 | "^\\.$",
15 | "^\\./(.*)/[A-Z]((?!\\.)(?!types.).)+$",
16 | "^\\./(.*)/((?!\\.)(?!types.).)+$",
17 | "/types$",
18 | "\\.(css|scss)$",
19 | "\\.(svg|png|jpg|jpeg|gif)$",
20 | ".*"
21 | ],
22 | "importOrderSeparation": false,
23 | "importOrderSortSpecifiers": true,
24 | "importOrderCaseInsensitive": true
25 | }
26 |
--------------------------------------------------------------------------------
/.storybook/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #efefef;
3 | }
4 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import type { StorybookConfig } from "@storybook/react-webpack5";
3 |
4 | const config: StorybookConfig = {
5 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
6 | addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app"],
7 | framework: {
8 | name: "@storybook/react-webpack5",
9 | options: {},
10 | },
11 | docs: {
12 | autodocs: "tag",
13 | },
14 | core: {
15 | builder: "@storybook/builder-webpack5",
16 | },
17 | webpackFinal: (config) => {
18 | config.module = config.module || { rules: [] };
19 | config.module.rules = config.module.rules || [];
20 |
21 | config.module.rules.push({
22 | test: /\.s(a|c)ss$/,
23 | include: path.resolve(__dirname, "../"),
24 | use: [
25 | "style-loader",
26 | {
27 | loader: "css-loader",
28 | options: {
29 | modules: {
30 | auto: true,
31 | localIdentName: "[name]__[local]--[hash:base64:5]",
32 | },
33 | },
34 | },
35 | "sass-loader",
36 | ],
37 | });
38 | config.module.rules.push({
39 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
40 | type: "asset/resource",
41 | });
42 | return config;
43 | },
44 | };
45 | export default config;
46 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 |
3 |
4 | export const parameters = {
5 | actions: { argTypesRegex: "^on[A-Z].*" },
6 | controls: {
7 | matchers: {
8 | color: /(background|color)$/i,
9 | date: /Date$/,
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Portola Labs, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
Open-source CSV and XLS/XLSX file importer for React and JavaScript
6 |
7 |
8 |
9 | ## How It Works
10 |
11 | 1. Embed the CSV Importer in your app with the [React](https://www.npmjs.com/package/csv-import-react)
12 | or [JavaScript](https://www.npmjs.com/package/csv-import-js) SDK
13 | 2. Define the columns your users can import (via the `template` parameter)
14 | 3. Your users import their files in your app
15 | 4. Retrieve the imported data from the `onComplete` event
16 |
17 | 
18 |
19 | ## Get Started
20 |
21 | ### 1. Install SDK
22 |
23 | Use NPM or Yarn to install the SDK for [React](https://www.npmjs.com/package/csv-import-react)
24 | or [JavaScript](https://www.npmjs.com/package/csv-import-js).
25 |
26 | **NPM**
27 |
28 | ```bash
29 | npm install csv-import-react
30 | # or
31 | npm install csv-import-js
32 | ```
33 |
34 | **Yarn**
35 |
36 | ```bash
37 | yarn add csv-import-react
38 | # or
39 | yarn add csv-import-js
40 | ```
41 |
42 | ### 2. Add the importer to your application
43 |
44 | #### Using React
45 |
46 | ```javascript
47 | import { CSVImporter } from "csv-import-react";
48 | import { useState } from "react";
49 |
50 | function MyComponent() {
51 | const [isOpen, setIsOpen] = useState(false);
52 |
53 | return (
54 | <>
55 |
56 |
57 | setIsOpen(false)}
60 | darkMode={true}
61 | onComplete={(data) => console.log(data)}
62 | template={{
63 | columns: [
64 | {
65 | name: "First Name",
66 | key: "first_name",
67 | required: true,
68 | description: "The first name of the user",
69 | suggested_mappings: ["First", "Name"],
70 | },
71 | {
72 | name: "Age",
73 | data_type: "number",
74 | },
75 | ],
76 | }}
77 | />
78 | >
79 | );
80 | }
81 | ```
82 |
83 | #### Using JavaScript
84 |
85 | ```html
86 |
87 |
88 |
89 |
90 |
91 |
92 |
120 |
121 | ```
122 |
123 | ## SDK Reference
124 |
125 | ### isModal (_boolean_, default: `true`)
126 |
127 | When set to `true` (default value), the importer will behave as a modal with its open state controlled by `modalIsOpen`. When set to `false`, the importer will be embedded directly in your page.
128 |
129 | ### modalIsOpen (_boolean_, default: `false`)
130 |
131 | Only used when `isModal` is `true`: Controls the importer modal being open or closed.
132 | \
133 | **React SDK Only**: For the JavaScript SDK, use `.showModal()` and `.closeModal()` to operate the modal.
134 |
135 | ### modalOnCloseTriggered (_function_)
136 |
137 | Only used when `isModal` is `true`: A function called when the user clicks the close button or clicks outside of (when used with `modalCloseOnOutsideClick`) the importer. `useState` can be used to control the importer modal opening and closing.
138 |
139 | ```javascript
140 | const [isOpen, setIsOpen] = useState(false);
141 | ```
142 |
143 | ```jsx
144 |
145 | setIsOpen(false)}
148 | ...
149 | />
150 | ```
151 |
152 | ### modalCloseOnOutsideClick (_boolean_, default: `false`)
153 |
154 | Only used when `isModal` is `true`: Clicking outside the modal will call the `modalOnCloseTriggered` function.
155 |
156 | ### template (_object_)
157 |
158 | Configure the columns used for the import.
159 |
160 | ```jsx
161 | template={{
162 | columns: [
163 | {
164 | name: "First Name",
165 | key: "first_name",
166 | required: true,
167 | description: "The first name of the user",
168 | suggested_mappings: ["First", "Name"],
169 | },
170 | {
171 | name: "Age",
172 | data_type: "number",
173 | },
174 | ],
175 | }}
176 | ```
177 |
178 | ### onComplete (_function_)
179 |
180 | Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data, column definitions, and other information about the import.
181 |
182 | ```jsx
183 | onComplete={(data) => console.log(data)}
184 | ```
185 |
186 | Example `data`:
187 |
188 | ```json
189 | {
190 | "num_rows": 2,
191 | "num_columns": 3,
192 | "columns": [
193 | {
194 | "key": "age",
195 | "name": "Age"
196 | },
197 | {
198 | "key": "email",
199 | "name": "Email"
200 | },
201 | {
202 | "key": "first_name",
203 | "name": "First Name"
204 | }
205 | ],
206 | "rows": [
207 | {
208 | "index": 0,
209 | "values": {
210 | "age": 23,
211 | "email": "maria@example.com",
212 | "first_name": "Maria"
213 | }
214 | },
215 | {
216 | "index": 1,
217 | "values": {
218 | "age": 32,
219 | "email": "robert@example.com",
220 | "first_name": "Robert"
221 | }
222 | }
223 | ]
224 | }
225 | ```
226 |
227 | ### darkMode (_boolean_, default: `false`)
228 |
229 | Toggle between dark mode (`true`) and light mode (`false`).
230 |
231 | ### primaryColor (_string_)
232 |
233 | Specifies the primary color for the importer in hex format. Use `customStyles` to customize the UI in more detail.
234 |
235 | ```jsx
236 | primaryColor="#7A5EF8"
237 | ```
238 |
239 | ### customStyles (_object_)
240 |
241 | Apply custom styles to the importer with an object containing CSS properties and values. Note that custom style properties will override `primaryColor` and any default styles from `darkMode`.
242 | Available options:
243 |
244 | ```jsx
245 | customStyles={{
246 | "font-family": "cursive",
247 | "font-size": "15px",
248 | "base-spacing": "2rem",
249 | "border-radius": "8px",
250 | "color-primary": "salmon",
251 | "color-primary-hover": "crimson",
252 | "color-secondary": "indianRed",
253 | "color-secondary-hover": "crimson",
254 | "color-tertiary": "indianRed",
255 | "color-tertiary-hover": "crimson",
256 | "color-border": "lightCoral",
257 | "color-text": "brown",
258 | "color-text-soft": "rgba(165, 42, 42, .5)",
259 | "color-text-on-primary": "#fff",
260 | "color-text-on-secondary": "#ffffff",
261 | "color-background": "bisque",
262 | "color-background-modal": "blanchedAlmond",
263 | "color-input-background": "blanchedAlmond",
264 | "color-input-background-soft": "white",
265 | "color-background-menu-hover": "bisque",
266 | "color-importer-link": "indigo",
267 | "color-progress-bar": "darkGreen"
268 | }}
269 | ```
270 |
271 | ## Internationalization
272 |
273 | ### Predefined languages
274 | - Out-of-the-box support for various languages.
275 | - Common languages are available through the language prop (i.e., `language="fr"` for French).
276 | - Available predefined languages:
277 | - en
278 | - es
279 | - fr
280 |
281 | ### Customizable language
282 | - Language keys can be exported and overridden.
283 | - Labels and messages can be customized to any text.
284 | - Translations key examples can be found in `src/i18n/es.ts`
285 |
286 | ```javascript
287 | // Set up custom translations
288 | const customTranslations = {
289 | jp: {
290 | Upload: "アップロード",
291 | "Browse files": "ファイルを参照",
292 | },
293 | pt: {
294 | Upload: "Carregar",
295 | "Browse files": "Procurar arquivos",
296 | },
297 | };
298 |
299 | return (
300 |
301 | )
302 |
303 | ```
304 |
305 | ### showDownloadTemplateButton (_boolean_, default: `true`)
306 |
307 | When set to `false`, hide the Download Template button on the first screen of the importer.
308 |
309 | ### skipHeaderRowSelection (_boolean_, default: `false`)
310 |
311 | When set to `true`, the importer will not display and skip the Header Row Selection step and always choose the first row in the file as the header.
312 |
313 | ## Contributing
314 |
315 | ### Setting Up the Project
316 |
317 | To set up the project locally, follow these steps:
318 |
319 | 1. **Clone the repository**
320 | ```bash
321 | git clone https://github.com/tableflowhq/csv-import.git
322 | cd csv-import
323 | ```
324 |
325 | 2. **Install dependencies**
326 | ```bash
327 | yarn install
328 | ```
329 |
330 | 3. **Build the project**
331 | ```bash
332 | yarn build
333 | ```
334 |
335 | ### Running Storybook
336 | To run Storybook locally, follow these steps:
337 |
338 | 1. **Start Storybook**
339 | ```bash
340 | yarn storybook
341 | ```
342 |
343 | 2. **Open Storybook in your browser:**
344 | Storybook should automatically open in your default browser. If it doesn't, navigate to [http://localhost:6006](http://localhost:6006).
345 |
346 | ### Modifying the project and testing with the demo app
347 |
348 | The project includes a demo app that you can use to test your changes. The demo app has its own `README.md` file with detailed instructions on how to set it up and run it.
349 |
350 | 1. Make your changes in the codebase.
351 | 2. Follow the instructions in the demo app's `README.md` to set up and run the demo app. This will help you verify that your changes work as expected in a real application.
352 | 3. Commit your changes and push them to your forked repository.
353 | 4. Create a pull request to the main repository.
354 |
355 | ## Get In Touch
356 |
357 | Let us know your feedback or feature requests! Submit a GitHub
358 | issue [here](https://github.com/tableflowhq/csv-import/issues/new).
359 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .yalc
25 | yalc.lock
26 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | ## How to use the Demo project
2 |
3 | 1. Make sure you have yalc installed, `yarn global add yalc`
4 | 2. In the project directory, you can build and then publish this package locally using:
5 | ```bash
6 | yarn build && yalc publish
7 | ```
8 | 3. cd to the demo/ folder and add the published package
9 | ```bash
10 | yalc add csv-import
11 | ```
12 |
13 | ### `yarn start`
14 |
15 | Runs the app in the development mode.\
16 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
17 |
18 | The page will reload when you make changes.\
19 | You may also see any lint errors in the console.
20 |
21 | ### `npm run build`
22 |
23 | Builds the app for production to the `build` folder.\
24 | It correctly bundles React in production mode and optimizes the build for the best performance.
25 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/demo/modal-test-js.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-sdk",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "assert": "^2.1.0",
10 | "constants": "^0.0.2",
11 | "crypto": "^1.0.1",
12 | "csv-import-react": "^1.0.6",
13 | "fs": "^0.0.1-security",
14 | "http": "^0.0.1-security",
15 | "https": "^1.0.0",
16 | "os": "^0.1.2",
17 | "path": "^0.12.7",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0",
20 | "react-scripts": "5.0.1",
21 | "stream": "^0.0.2",
22 | "tty": "^1.0.1",
23 | "url": "^0.11.3",
24 | "util": "^0.12.5",
25 | "web-vitals": "^2.1.4",
26 | "zlib": "^1.0.5"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/demo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/logo192.png
--------------------------------------------------------------------------------
/demo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/logo512.png
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/demo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/demo/src/App.css:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: var(--color-primary);
3 | border: none;
4 | color: white;
5 | padding: 15px 32px;
6 | text-align: center;
7 | text-decoration: none;
8 | display: inline-block;
9 | font-size: 16px;
10 | }
11 |
12 | .App {
13 | height: 100vh;
14 | width: 100vw;
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | text-align: center;
19 | }
20 |
21 | .App-logo {
22 | height: 40vmin;
23 | pointer-events: none;
24 | }
25 |
26 | @media (prefers-reduced-motion: no-preference) {
27 | .App-logo {
28 | animation: App-logo-spin infinite 20s linear;
29 | }
30 | }
31 |
32 | .App-header {
33 | background-color: #282c34;
34 | min-height: 100vh;
35 | display: flex;
36 | flex-direction: column;
37 | align-items: center;
38 | justify-content: center;
39 | font-size: calc(10px + 2vmin);
40 | color: white;
41 | }
42 |
43 | .App-link {
44 | color: #61dafb;
45 | }
46 |
47 | @keyframes App-logo-spin {
48 | from {
49 | transform: rotate(0deg);
50 | }
51 | to {
52 | transform: rotate(360deg);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import { CSVImporter } from "csv-import-react";
2 | import { useState } from "react";
3 | import "./App.css";
4 |
5 | function App() {
6 | const template = {
7 | columns: [
8 | {
9 | name: "First Name",
10 | key: "first_name",
11 | required: true,
12 | description: "The first name of the user",
13 | suggested_mappings: ["first", "mame"],
14 | },
15 | {
16 | name: "Last Name",
17 | suggested_mappings: ["last"],
18 | },
19 | {
20 | name: "Email",
21 | required: true,
22 | description: "The email of the user",
23 | },
24 | ],
25 | };
26 | const [isOpen, setIsOpen] = useState(false);
27 | return (
28 |
29 |
32 |
33 | setIsOpen(false)}
39 | modalCloseOnOutsideClick={true}
40 | />
41 |
42 | );
43 | }
44 |
45 | export default App;
46 |
--------------------------------------------------------------------------------
/demo/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/demo/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/demo/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/package-js.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csv-import-js",
3 | "version": "1.0.14",
4 | "description": "Open-source CSV and XLS/XLSX file importer for React and JavaScript",
5 | "main": "index.js",
6 | "files": ["."],
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/tableflowhq/csv-import.git"
10 | },
11 | "keywords": ["csv", "import", "excel", "data", "importer", "react", "tableflow", "csv-import"],
12 | "author": "TableFlow",
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/tableflowhq/csv-import/issues"
16 | },
17 | "homepage": "https://github.com/tableflowhq/csv-import#readme",
18 | "types": "index.d.ts",
19 | "peerDependencies": {
20 | "react": ">=18.0.0",
21 | "react-dom": ">=18.0.0"
22 | },
23 | "devDependencies": {
24 | "@babel/preset-env": "^7.22.4",
25 | "@babel/preset-react": "^7.22.3",
26 | "@babel/preset-typescript": "^7.21.5",
27 | "@rollup/plugin-commonjs": "^17.1.0",
28 | "@rollup/plugin-image": "^2.1.1",
29 | "@rollup/plugin-node-resolve": "^11.2.1",
30 | "@rollup/plugin-replace": "^5.0.5",
31 | "@storybook/addon-essentials": "7.0.18",
32 | "@storybook/addon-interactions": "7.0.18",
33 | "@storybook/addon-links": "7.0.18",
34 | "@storybook/blocks": "7.0.18",
35 | "@storybook/react": "7.0.18",
36 | "@storybook/react-webpack5": "7.0.18",
37 | "@storybook/testing-library": "0.0.14-next.2",
38 | "@testing-library/jest-dom": "^5.14.1",
39 | "@testing-library/react": "^11.2.7",
40 | "@trivago/prettier-plugin-sort-imports": "^4.1.1",
41 | "@types/babel__core": "^7.20.1",
42 | "@types/jest": "^24.9.1",
43 | "@types/papaparse": "^5.3.14",
44 | "@types/react": "^18.0.0",
45 | "@types/react-dom": "^18.0.0",
46 | "@types/use-sync-external-store": "0.0.3",
47 | "@types/xlsx": "^0.0.36",
48 | "@typescript-eslint/eslint-plugin": "^5.56.0",
49 | "@typescript-eslint/parser": "^5.56.0",
50 | "core-js": "^3.22.7",
51 | "css-loader": "^6.8.1",
52 | "eslint-config-prettier": "^8.8.0",
53 | "eslint-plugin-prettier": "^4.2.1",
54 | "eslint-plugin-react": "^7.32.2",
55 | "identity-obj-proxy": "^3.0.0",
56 | "jest": "^26.6.3",
57 | "node-sass": "^9.0.0",
58 | "prettier": "^2.8.6",
59 | "prop-types": "^15.8.1",
60 | "react": "18.2.0",
61 | "react-dom": "18.2.0",
62 | "rollup": "^2.56.3",
63 | "rollup-plugin-copy": "^3.4.0",
64 | "rollup-plugin-peer-deps-external": "^2.2.4",
65 | "rollup-plugin-postcss": "^3.1.8",
66 | "rollup-plugin-typescript2": "^0.29.0",
67 | "sass": "^1.69.7",
68 | "sass-loader": "^13.3.3",
69 | "storybook": "7.0.18",
70 | "style-loader": "^3.3.3",
71 | "ts-jest": "^26.5.6",
72 | "typescript": "^4.5.5",
73 | "typescript-plugin-css-modules": "^3.4.0",
74 | "use-sync-external-store": "^1.2.0"
75 | },
76 | "dependencies": {
77 | "@chakra-ui/alert": "^2.2.2",
78 | "@chakra-ui/button": "^2.1.0",
79 | "@chakra-ui/react": "^2.8.1",
80 | "@chakra-ui/system": "^2.6.2",
81 | "@emotion/react": "^11.11.3",
82 | "@emotion/styled": "^11.11.0",
83 | "@rollup/plugin-json": "^6.1.0",
84 | "framer-motion": "^10.17.12",
85 | "papaparse": "^5.4.1",
86 | "react-dropzone": "^14.2.3",
87 | "react-icons": "^4.12.0",
88 | "xlsx": "^0.18.5",
89 | "zustand": "^4.4.7"
90 | },
91 | "type": "module"
92 | }
93 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csv-import-react",
3 | "version": "1.0.14",
4 | "description": "Open-source CSV and XLS/XLSX file importer for React and JavaScript",
5 | "main": "build/index.js",
6 | "module": "build/index.esm.js",
7 | "files": [
8 | "build"
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/tableflowhq/csv-import.git"
13 | },
14 | "keywords": [
15 | "csv",
16 | "import",
17 | "excel",
18 | "data",
19 | "importer",
20 | "react",
21 | "tableflow",
22 | "csv-import"
23 | ],
24 | "author": "TableFlow",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/tableflowhq/csv-import/issues"
28 | },
29 | "homepage": "https://github.com/tableflowhq/csv-import#readme",
30 | "types": "build/index.d.ts",
31 | "scripts": {
32 | "build:js": "rollup --config rollup.config-js.js && cp package-js.json build/package.json",
33 | "build:react": "rollup --config rollup.config.js",
34 | "publish:local:js": "yarn build:js && cd build && yalc publish",
35 | "publish:local:react": "yarn build:react && yalc publish",
36 | "publish:js": "yarn build:js && cd build && npm publish",
37 | "publish:react": "yarn build:react && npm publish",
38 | "build:watch": "rollup -c -w",
39 | "storybook": "storybook dev -p 6006",
40 | "storybook:export": "build-storybook",
41 | "generate": "node ./util/create-component",
42 | "build-storybook": "storybook build"
43 | },
44 | "peerDependencies": {
45 | "react": ">=18.0.0",
46 | "react-dom": ">=18.0.0"
47 | },
48 | "devDependencies": {
49 | "@babel/preset-env": "^7.22.4",
50 | "@babel/preset-react": "^7.22.3",
51 | "@babel/preset-typescript": "^7.21.5",
52 | "@rollup/plugin-commonjs": "^17.1.0",
53 | "@rollup/plugin-image": "^2.1.1",
54 | "@rollup/plugin-node-resolve": "^11.2.1",
55 | "@rollup/plugin-replace": "^5.0.5",
56 | "@storybook/addon-essentials": "7.0.18",
57 | "@storybook/addon-interactions": "7.0.18",
58 | "@storybook/addon-links": "7.0.18",
59 | "@storybook/blocks": "7.0.18",
60 | "@storybook/react": "7.0.18",
61 | "@storybook/react-webpack5": "7.0.18",
62 | "@storybook/testing-library": "0.0.14-next.2",
63 | "@trivago/prettier-plugin-sort-imports": "^4.1.1",
64 | "@types/babel__core": "^7.20.1",
65 | "@types/jest": "^24.9.1",
66 | "@types/papaparse": "^5.3.14",
67 | "@types/react": "^18.0.0",
68 | "@types/react-dom": "^18.0.0",
69 | "@types/use-sync-external-store": "0.0.3",
70 | "@types/xlsx": "^0.0.36",
71 | "@typescript-eslint/eslint-plugin": "^5.56.0",
72 | "@typescript-eslint/parser": "^5.56.0",
73 | "core-js": "^3.22.7",
74 | "css-loader": "^6.8.1",
75 | "eslint-config-prettier": "^8.8.0",
76 | "eslint-plugin-prettier": "^4.2.1",
77 | "eslint-plugin-react": "^7.32.2",
78 | "identity-obj-proxy": "^3.0.0",
79 | "jest": "^26.6.3",
80 | "node-sass": "^9.0.0",
81 | "prettier": "^2.8.6",
82 | "prop-types": "^15.8.1",
83 | "react": "18.2.0",
84 | "react-dom": "18.2.0",
85 | "rollup": "^2.56.3",
86 | "rollup-plugin-copy": "^3.4.0",
87 | "rollup-plugin-peer-deps-external": "^2.2.4",
88 | "rollup-plugin-postcss": "^3.1.8",
89 | "rollup-plugin-typescript2": "^0.29.0",
90 | "sass": "^1.69.7",
91 | "sass-loader": "^13.3.3",
92 | "storybook": "7.0.18",
93 | "style-loader": "^3.3.3",
94 | "ts-jest": "^26.5.6",
95 | "typescript": "^4.5.5",
96 | "typescript-plugin-css-modules": "^3.4.0",
97 | "use-sync-external-store": "^1.2.0"
98 | },
99 | "dependencies": {
100 | "@chakra-ui/alert": "^2.2.2",
101 | "@chakra-ui/button": "^2.1.0",
102 | "@chakra-ui/react": "^2.8.1",
103 | "@chakra-ui/system": "^2.6.2",
104 | "@emotion/cache": "^11.11.0",
105 | "@emotion/react": "^11.11.3",
106 | "@emotion/styled": "^11.11.0",
107 | "@rollup/plugin-json": "^6.1.0",
108 | "framer-motion": "^10.17.12",
109 | "i18next": "^23.10.1",
110 | "papaparse": "^5.4.1",
111 | "react-dropzone": "^14.2.3",
112 | "react-i18next": "^14.1.0",
113 | "react-icons": "^4.12.0",
114 | "xlsx": "^0.18.5",
115 | "zustand": "^4.4.7"
116 | },
117 | "type": "module"
118 | }
119 |
--------------------------------------------------------------------------------
/rollup.config-js.js:
--------------------------------------------------------------------------------
1 | import peerDepsExternal from "rollup-plugin-peer-deps-external";
2 |
3 | import postcss from "rollup-plugin-postcss";
4 | import typescript from "rollup-plugin-typescript2";
5 | import commonjs from "@rollup/plugin-commonjs";
6 | import image from "@rollup/plugin-image";
7 | import json from "@rollup/plugin-json";
8 | import resolve, { nodeResolve } from "@rollup/plugin-node-resolve";
9 | import replace from "@rollup/plugin-replace";
10 |
11 | const packageJson = require("./package.json");
12 |
13 | export default {
14 | input: "src/js.tsx",
15 | output: [
16 | {
17 | file: "build/index.js",
18 | format: "umd",
19 | name: "CSVImporter",
20 | sourcemap: true,
21 | },
22 | ],
23 | plugins: [
24 | replace({
25 | "process.env.NODE_ENV": JSON.stringify("production"),
26 | preventAssignment: true,
27 | }),
28 | resolve({
29 | browser: true,
30 | }),
31 | commonjs(),
32 | typescript({ useTsconfigDeclarationDir: true }),
33 | image(),
34 | postcss({}),
35 | json(),
36 | ],
37 | };
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import peerDepsExternal from "rollup-plugin-peer-deps-external";
2 | import postcss from "rollup-plugin-postcss";
3 | import typescript from "rollup-plugin-typescript2";
4 | import commonjs from "@rollup/plugin-commonjs";
5 | import image from "@rollup/plugin-image";
6 | import json from "@rollup/plugin-json";
7 | import resolve from "@rollup/plugin-node-resolve";
8 |
9 | const packageJson = require("./package.json");
10 |
11 | export default {
12 | input: "src/index.ts",
13 | output: [
14 | {
15 | file: packageJson.main,
16 | format: "cjs",
17 | sourcemap: true,
18 | },
19 | {
20 | file: packageJson.module,
21 | format: "esm",
22 | sourcemap: true,
23 | },
24 | ],
25 | external: ["react", "react-dom", "react/jsx-runtime", "@emotion/react"],
26 | plugins: [
27 | peerDepsExternal(),
28 | resolve({
29 | browser: true,
30 | }),
31 | commonjs(),
32 | typescript({ useTsconfigDeclarationDir: true }),
33 | image(),
34 | postcss({}),
35 | json(),
36 | ],
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/CSVImporter/CSVImporter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { ComponentMeta, ComponentStory, Story } from "@storybook/react";
3 | import defaults from "../../settings/defaults";
4 | import { CSVImporterProps } from "../../types";
5 | import ImporterComponent from "./index";
6 |
7 | export default {
8 | title: "User Interface/Importer",
9 | component: ImporterComponent,
10 | argTypes: {
11 | primaryColor: {
12 | control: { type: "color" },
13 | },
14 | labelColor: {
15 | control: { type: "color" },
16 | },
17 | },
18 | } as ComponentMeta;
19 |
20 | const template = {
21 | columns: [
22 | {
23 | name: "First Name",
24 | key: "first_name",
25 | required: true,
26 | description: "The first name of the user",
27 | suggested_mappings: ["first", "mame"],
28 | },
29 | {
30 | name: "Last Name",
31 | suggested_mappings: ["last"],
32 | },
33 | {
34 | name: "Email",
35 | required: true,
36 | description: "The email of the user",
37 | },
38 | ],
39 | };
40 |
41 | const Template: Story = (args: CSVImporterProps) => {
42 | const [isOpen, setIsOpen] = useState(false);
43 |
44 | const { isModal } = args;
45 |
46 | const props = {
47 | ...(isModal ? { modalIsOpen: isOpen } : {}),
48 | ...(isModal ? { modalOnCloseTriggered: () => setIsOpen(false) } : {}),
49 | ...(isModal ? { modalCloseOnOutsideClick: args.modalCloseOnOutsideClick } : {}),
50 | ...args,
51 | };
52 |
53 | return (
54 |
55 | {args.isModal && }
56 |
57 |
58 | );
59 | };
60 |
61 | export const Importer = Template.bind({});
62 | Importer.args = {
63 | language: "en",
64 | ...defaults,
65 | template: template,
66 | customTranslations: {
67 | jp: {
68 | Upload: "アップロード",
69 | "Browse files": "ファイルを参照",
70 | },
71 | pt: {
72 | Upload: "Carregar",
73 | "Browse files": "Procurar arquivos",
74 | },
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/src/components/CSVImporter/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useEffect, useRef, useState } from "react";
2 | import { useTranslation } from "react-i18next";
3 | import "../../i18n/i18n";
4 | import Importer from "../../importer/features/main";
5 | import Providers from "../../importer/providers";
6 | import useThemeStore from "../../importer/stores/theme";
7 | import { darkenColor, isValidColor } from "../../importer/utils/utils";
8 | import { CSVImporterProps } from "../../types";
9 | import "../../importer/style/index.scss";
10 | import "./style/csv-importer.css";
11 |
12 | const CSVImporter = forwardRef((importerProps: CSVImporterProps, forwardRef?: any) => {
13 | const {
14 | isModal = true,
15 | modalIsOpen = true,
16 | modalOnCloseTriggered = () => null,
17 | modalCloseOnOutsideClick,
18 | template,
19 | darkMode = false,
20 | primaryColor = "#7a5ef8",
21 | className,
22 | onComplete,
23 | customStyles,
24 | showDownloadTemplateButton,
25 | skipHeaderRowSelection,
26 | language,
27 | customTranslations,
28 | ...props
29 | } = importerProps;
30 | const ref = forwardRef ?? useRef(null);
31 |
32 | const { t, i18n } = useTranslation();
33 | const current = ref?.current as any;
34 |
35 | useEffect(() => {
36 | i18n.changeLanguage(language);
37 | }, [language]);
38 |
39 | useEffect(() => {
40 | if (customTranslations) {
41 | Object.keys(customTranslations).forEach((language) => {
42 | i18n.addResourceBundle(language, "translation", customTranslations[language], true, true);
43 | });
44 | }
45 | }, [customTranslations]);
46 |
47 | useEffect(() => {
48 | if (isModal && current) {
49 | if (modalIsOpen) current?.showModal?.();
50 | else current?.close?.();
51 | }
52 | }, [isModal, modalIsOpen, current]);
53 | const baseClass = "CSVImporter";
54 | const themeClass = darkMode ? `${baseClass}-dark` : `${baseClass}-light`;
55 | const domElementClass = ["csv-importer", `${baseClass}-${isModal ? "dialog" : "div"}`, themeClass, className].filter((i) => i).join(" ");
56 |
57 | // Set Light/Dark mode
58 | const setTheme = useThemeStore((state) => state.setTheme);
59 |
60 | useEffect(() => {
61 | const theme = darkMode ? "dark" : "light";
62 | setTheme(theme);
63 | }, [darkMode]);
64 |
65 | // Apply primary color
66 | useEffect(() => {
67 | if (primaryColor && isValidColor(primaryColor)) {
68 | const root = document.documentElement;
69 | root.style.setProperty("--color-primary", primaryColor);
70 | root.style.setProperty("--color-primary-hover", darkenColor(primaryColor, 20));
71 | }
72 | }, [primaryColor]);
73 |
74 | const backdropClick = (event: { target: any }) => {
75 | if (modalCloseOnOutsideClick && event.target === current) {
76 | modalOnCloseTriggered();
77 | }
78 | };
79 |
80 | current?.addEventListener("cancel", () => {
81 | modalOnCloseTriggered();
82 | });
83 |
84 | const elementProps = {
85 | ref,
86 | ...(isModal ? { onClick: backdropClick } : {}),
87 | className: domElementClass,
88 | "data-theme": darkMode ? "dark" : "light",
89 | style: { colorScheme: darkMode ? "dark" : "light" },
90 | ...props,
91 | };
92 |
93 | const ImporterComponent = () => (
94 |
95 |
96 |
97 | );
98 |
99 | return isModal ? (
100 |
101 |
104 |
105 | ) : (
106 |
107 |
108 |
109 | );
110 | });
111 |
112 | export default CSVImporter;
113 |
--------------------------------------------------------------------------------
/src/components/CSVImporter/style/csv-importer.css:
--------------------------------------------------------------------------------
1 | .CSVImporter {
2 | border: none;
3 | background-color: transparent;
4 | padding: 0 1rem;
5 | border-radius: 1.2rem;
6 | color: inherit;
7 | cursor: pointer;
8 | font-weight: 500;
9 | font-size: 14px;
10 | /* height: 2.4rem; */
11 | display: inline-flex;
12 | gap: 0.5rem;
13 | align-items: center;
14 | transition: filter 0.2s ease-out;
15 | }
16 |
17 | .CSVImporter svg {
18 | display: block;
19 | }
20 |
21 | .CSVImporter svg path {
22 | stroke: currentColor !important;
23 | }
24 |
25 | .CSVImporter:hover,
26 | .CSVImporter:active {
27 | filter: brightness(1.2);
28 | }
29 |
30 | .CSVImporter-dialog::backdrop {
31 | background-color: rgba(0, 0, 0, 0.85);
32 | }
33 |
34 | .CSVImporter-dialog {
35 | border-radius: 1rem;
36 | width: 80vw;
37 | height: 100vh;
38 | min-width: 907px;
39 | border: none;
40 | position: fixed;
41 | inset: 0;
42 | padding: 0;
43 | margin: auto;
44 | }
45 |
46 | .CSVImporter-div {
47 | border: none;
48 | display: block;
49 | width: 100%;
50 | height: 98vh;
51 | overflow-y: hidden;
52 | }
53 |
54 | @media (max-width: 768px) {
55 | .CSVImporter-dialog {
56 | width: 90vw;
57 | min-width: 950px;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/i18n/de.ts:
--------------------------------------------------------------------------------
1 | const translations = {
2 | Upload: "Hochladen",
3 | "Select Header": "Kopfzeile auswählen",
4 | "Map Columns": "Spalten zuordnen",
5 | "Expected Column": "Erwartete Spalten",
6 | Required: "Erforderlich",
7 | "Drop your file here": "Datei hier ablegen",
8 | or: "oder",
9 | "Browse files": "Dateien durchsuchen",
10 | "Download Template": "Vorlage herunterladen",
11 | "Only CSV, XLS, and XLSX files can be uploaded": "Nur CSV-, XLS- und XLSX-Dateien können hochgeladen werden",
12 | "Select Header Row": "Kopfzeilenreihe auswählen",
13 | "Select the row which contains the column headers": "Wähle die Zeile, die die Spaltenüberschriften enthält",
14 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Nur das erste Blatt ("{{sheet}}") der Excel-Datei wird importiert. Um mehrere Blätter zu importieren, lade bitte jedes Blatt einzeln hoch.",
15 | "Cancel": "Abbrechen",
16 | "Continue": "Weiter",
17 | "Your File Column": "Deine Spalte der Datei",
18 | "Your Sample Data": "Deine Beispieldaten",
19 | "Destination Column": "Zielspalte",
20 | "Include": "Einfügen",
21 | "Submit": "Senden",
22 | "Loading...": "Laden...",
23 | "Please include all required columns": "Bitte alle erforderlichen Spalten einfügen",
24 | "Back": "Zurück",
25 | "- Select one -": "- Wähle eine aus -",
26 | "- empty -": "- leer -",
27 | "Import Successful": "Import erfolgreich",
28 | "Upload Successful": "Upload erfolgreich",
29 | "Done": "Fertig",
30 | "Upload another file": "Eine weitere Datei hochladen",
31 | };
32 |
33 | export default translations;
34 |
--------------------------------------------------------------------------------
/src/i18n/es.ts:
--------------------------------------------------------------------------------
1 | const translations = {
2 | Upload: "Subir",
3 | "Select Header": "Seleccionar encabezado",
4 | "Map Columns": "Mapear columnas",
5 | "Expected Column": "Columnas esperadas",
6 | Required: "Requerido",
7 | "Drop your file here": "Suelta tu archivo aquí",
8 | or: "o",
9 | "Browse files": "Examinar archivos",
10 | "Download Template": "Descargar plantilla",
11 | "Only CSV, XLS, and XLSX files can be uploaded": "Solo se pueden subir archivos CSV, XLS y XLSX",
12 | "Select Header Row": "Seleccionar fila de encabezado",
13 | "Select the row which contains the column headers": "Selecciona la fila que contiene los encabezados de las columnas",
14 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": " Solo se importará la primera hoja ("{{sheet}}") del archivo de Excel. Para importar varias hojas, sube cada hoja individualmente.",
15 | "Cancel": "Cancelar",
16 | "Continue": "Continuar",
17 | "Your File Column": "Tu columna del archivo",
18 | "Your Sample Data": "Tus datos de muestra",
19 | "Destination Column": "Columna de destino",
20 | "Include": "Incluir",
21 | "Submit": "Enviar",
22 | "Loading...": "Cargando...",
23 | "Please include all required columns": "Por favor incluye todas las columnas requeridas",
24 | "Back": "Atrás",
25 | "- Select one -": "- Selecciona uno -",
26 | "- empty -": "- vacío -",
27 | "Import Successful": "Importación exitosa",
28 | "Upload Successful": "Se ha subido con éxito",
29 | "Done": "Listo",
30 | "Upload another file": "Subir otro archivo",
31 | };
32 |
33 | export default translations;
34 |
--------------------------------------------------------------------------------
/src/i18n/fr.ts:
--------------------------------------------------------------------------------
1 | // Translations in french
2 | //TODO: Double the translations
3 | const translations = {
4 | Upload: "Télécharger",
5 | "Select Header": "Sélectionner l'en-tête",
6 | "Map Columns": "Mapper les colonnes",
7 | "Expected Column": "Colonne attendue",
8 | Required: "Requis",
9 | "Drop your file here": "Déposez votre fichier ici",
10 | or: "ou",
11 | "Browse files": "Parcourir les fichiers",
12 | "Download Template": "Télécharger le modèle",
13 | "Only CSV, XLS, and XLSX files can be uploaded": "Seuls les fichiers CSV, XLS et XLSX peuvent être téléchargés",
14 | "Select Header Row": "Sélectionner la ligne d'en-tête",
15 | "Select the row which contains the column headers": "Sélectionnez la ligne qui contient les en-têtes de colonne",
16 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Seule la première feuille ("{{sheet}}") du fichier Excel sera importée. Pour importer plusieurs feuilles, veuillez télécharger chaque feuille individuellement.",
17 | "Cancel": "Annuler",
18 | "Continue": "Continuer",
19 | "Your File Column": "Votre colonne de fichier",
20 | "Your Sample Data": "Vos données d'échantillon",
21 | "Destination Column": "Colonne de destination",
22 | "Include": "Inclure",
23 | "Submit": "Soumettre",
24 | "Loading...": "Chargement...",
25 | "Please include all required columns": "Veuillez inclure toutes les colonnes requises",
26 | "Back": "Retour",
27 | "- Select one -": "- Sélectionnez un -",
28 | "- empty -": "- vide -",
29 | "Import Successful": "Importation réussie",
30 | "Upload Successful": "Téléchargement réussi",
31 | "Done": "Terminé",
32 | "Upload another file": "Télécharger un autre fichier",
33 | };
34 |
35 | export default translations;
36 |
--------------------------------------------------------------------------------
/src/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18, { Resource } from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 | import esTranslation from "./es";
4 | import frTranslation from "./fr";
5 | import itTranslations from "./it";
6 | import deTranslations from "./de";
7 |
8 | const resources: Resource = {
9 | en: {
10 | translation: {},
11 | },
12 | fr: {
13 | translation: frTranslation,
14 | },
15 | es: {
16 | translation: esTranslation,
17 | },
18 | it: {
19 | translation: itTranslations,
20 | },
21 | de: {
22 | translation: deTranslations,
23 | },
24 | };
25 |
26 | i18.use(initReactI18next).init({
27 | resources,
28 | lng: "en",
29 | keySeparator: false,
30 | interpolation: {
31 | escapeValue: false,
32 | },
33 | });
34 |
35 | export default i18;
36 |
--------------------------------------------------------------------------------
/src/i18n/it.ts:
--------------------------------------------------------------------------------
1 | // Translations in Italian
2 | const translations = {
3 | Upload: "Caricare",
4 | "Select Header": "Seleziona intestazione",
5 | "Map Columns": "Mappa colonne",
6 | "Expected Column": "Colonna prevista",
7 | Required: "Richiesto",
8 | "Drop your file here": "Trascina il tuo file qui",
9 | or: "oppure",
10 | "Browse files": "Sfoglia file",
11 | "Download Template": "Scarica il modello",
12 | "Only CSV, XLS, and XLSX files can be uploaded": "Solo i file CSV, XLS e XLSX possono essere caricati",
13 | "Select Header Row": "Seleziona la riga di intestazione",
14 | "Select the row which contains the column headers": "Seleziona la riga che contiene le intestazioni delle colonne",
15 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Solo il primo foglio ("{{sheet}}") del file Excel verrà importato. Per importare più fogli, carica ogni foglio singolarmente.",
16 | "Cancel": "Annulla",
17 | "Continue": "Continua",
18 | "Your File Column": "La tua colonna di file",
19 | "Your Sample Data": "I tuoi dati di esempio",
20 | "Destination Column": "Colonna di destinazione",
21 | "Include": "Includere",
22 | "Submit": "Invia",
23 | "Loading...": "Caricamento...",
24 | "Please include all required columns": "Si prega di includere tutte le colonne richieste",
25 | "Back": "Indietro",
26 | "- Select one -": "- Selezionane uno -",
27 | "- empty -": "- vuoto -",
28 | "Import Successful": "Importazione riuscita",
29 | "Upload Successful": "Caricamento riuscito",
30 | "Done": "Fatto",
31 | "Upload another file": "Carica un altro file",
32 | };
33 |
34 | export default translations;
35 |
--------------------------------------------------------------------------------
/src/importer/components/Box/index.tsx:
--------------------------------------------------------------------------------
1 | import classes from "../../utils/classes";
2 | import { BoxProps } from "./types";
3 | import style from "./style/Box.module.scss";
4 |
5 | export default function Box({ className, variants = [], ...props }: BoxProps) {
6 | const variantStyles = classes(variants.map((c: keyof typeof style) => style[c]));
7 | const containerClasses = classes([style.box, variantStyles, className]);
8 |
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/src/importer/components/Box/style/Box.module.scss:
--------------------------------------------------------------------------------
1 | .box {
2 | display: block;
3 | margin: 0 auto;
4 | padding: var(--m);
5 | background-color: var(--color-background-modal);
6 | border-radius: var(--border-radius-5);
7 | box-shadow: 0 0 20px var(--color-background-modal-shadow);
8 | max-width: 100%;
9 |
10 | &.fluid {
11 | max-width: none;
12 | }
13 | &.mid {
14 | max-width: 440px;
15 | }
16 | &.wide {
17 | max-width: 660px;
18 | }
19 | &.space-l {
20 | padding: var(--m-l);
21 | }
22 | &.space-mid {
23 | padding: var(--m);
24 | }
25 | &.space-none {
26 | padding: 0;
27 | }
28 | &.bg-shade {
29 | background-color: var(--color-background-modal-shade);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/importer/components/Box/types/index.ts:
--------------------------------------------------------------------------------
1 | export type BoxVariant = "fluid" | "mid" | "wide" | "space-l" | "space-mid" | "space-none" | "bg-shade";
2 |
3 | export type BoxProps = React.HTMLAttributes & {
4 | variants?: BoxVariant[];
5 | };
6 |
7 | export const boxVariants: BoxVariant[] = ["fluid", "mid", "wide", "space-l", "space-mid", "space-none", "bg-shade"];
8 |
--------------------------------------------------------------------------------
/src/importer/components/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import classes from "../../utils/classes";
2 | import { CheckboxProps } from "./types";
3 | import style from "./style/Checkbox.module.scss";
4 |
5 | export default function Checkbox({ label, className, ...props }: CheckboxProps) {
6 | const containerClasses = classes([style.container, className]);
7 |
8 | return (
9 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/importer/components/Checkbox/style/Checkbox.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: inline-block;
3 | gap: var(--m-xs);
4 | align-items: center;
5 |
6 | &:has(input:not(:disabled)) {
7 | cursor: pointer;
8 | }
9 |
10 | input[type="checkbox"] {
11 | -webkit-appearance: none;
12 | appearance: none;
13 | background-color: transparent;
14 | margin: 0;
15 | color: var(--color-primary);
16 | width: var(--m);
17 | height: var(--m);
18 | border: 2px solid var(--color-border);
19 | display: grid;
20 | place-content: center;
21 | border-radius: var(--border-radius-1);
22 | cursor: pointer;
23 |
24 | &::before {
25 | content: "";
26 | width: var(--m-xs);
27 | height: var(--m-xs);
28 | }
29 |
30 | &:checked {
31 | background-color: var(--color-primary);
32 | border-color: var(--color-primary);
33 | }
34 |
35 | &:checked::before {
36 | clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
37 | box-shadow: inset 1em 1em var(--color-text-on-primary);
38 | }
39 |
40 | &:not(:disabled) {
41 | &:focus-visible {
42 | outline: 1px solid var(--color-border);
43 | outline-offset: 3px;
44 | }
45 | }
46 |
47 | &:disabled {
48 | --container-color: var(--container-disabled);
49 | color: var(--container-disabled);
50 | cursor: default;
51 | background-color: var(--color-input-disabled);
52 | border-color: var(--color-border-soft);
53 |
54 | &:checked {
55 | &::before {
56 | clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
57 | box-shadow: inset 1em 1em var(--color-border-soft);
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/importer/components/Checkbox/types/index.ts:
--------------------------------------------------------------------------------
1 | import { InputHTMLAttributes, ReactElement } from "react";
2 |
3 | export type CheckboxProps = InputHTMLAttributes & {
4 | label?: string | ReactElement;
5 | };
6 |
--------------------------------------------------------------------------------
/src/importer/components/Errors/index.tsx:
--------------------------------------------------------------------------------
1 | import { sizes } from "../../settings/theme";
2 | import classes from "../../utils/classes";
3 | import style from "./style/Errors.module.scss";
4 | import { PiInfo } from "react-icons/pi";
5 |
6 | export default function Errors({ error, centered = false }: { error?: unknown; centered?: boolean }) {
7 | return error ? (
8 |
9 |
10 |
11 | {error.toString()}
12 |
13 |
14 | ) : null;
15 | }
16 |
--------------------------------------------------------------------------------
/src/importer/components/Errors/style/Errors.module.scss:
--------------------------------------------------------------------------------
1 | .errors {
2 | color: var(--color-text-error);
3 | margin: var(--m-xxs) 0;
4 |
5 | p {
6 | margin: 0;
7 | display: flex;
8 | align-items: center;
9 | gap: var(--m-xxs);
10 | text-align: left;
11 | }
12 | }
13 |
14 | .centered {
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | flex-direction: column;
19 | height: 100%;
20 | }
21 |
--------------------------------------------------------------------------------
/src/importer/components/Input/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import { Portal } from "@chakra-ui/react";
3 | import useClickOutside from "../../hooks/useClickOutside";
4 | import useRect from "../../hooks/useRect";
5 | import useWindowSize from "../../hooks/useWindowSize";
6 | import classes from "../../utils/classes";
7 | import { InputProps } from "./types";
8 | import style from "./style/Input.module.scss";
9 | import { PiCaretDown, PiInfo } from "react-icons/pi";
10 |
11 | export default function Input({ as = "input", label, icon, iconAfter, error, options, className, variants = [], children, ...props }: InputProps) {
12 | const Element = as;
13 |
14 | const variantStyles = classes(variants.map((c: keyof typeof style) => style[c]));
15 |
16 | const containerClassName = classes([style.container, variantStyles, className]);
17 |
18 | const icon1 = icon && {icon};
19 |
20 | const icon2 = iconAfter ? (
21 | {iconAfter}
22 | ) : (
23 | error && (
24 |
25 |
26 |
27 | )
28 | );
29 |
30 | const iconSelect = options && (
31 |
32 |
33 |
34 | );
35 |
36 | const selectElement = options && options && ;
37 |
38 | const inputWrapper = (
39 |
40 | {icon1}
41 | {selectElement || }
42 | {iconSelect}
43 | {icon2}
44 |
45 | );
46 |
47 | return (
48 |
49 |
53 | {error &&
{error}
}
54 | {children &&
{children}
}
55 |
56 | );
57 | }
58 |
59 | function Select({ options = {}, placeholder, ...props }: InputProps) {
60 | const [open, setOpen] = useState(false);
61 |
62 | const onChangeOption = (e: any) => {
63 | const { value } = e.target;
64 | props?.onChange && props?.onChange(value);
65 | e.stopPropagation();
66 | e.preventDefault();
67 | onBlur();
68 | };
69 |
70 | const selectedKey = Object.keys(options).find((k) => options[k].value === props.value) || "";
71 |
72 | const [setRef, size, updateRect] = useRect();
73 | const [setRefPortal, sizePortal, updatePortalRect] = useRect();
74 | const windowSize = useWindowSize();
75 | const top = size.y + sizePortal.height > windowSize[1] - 4 ? windowSize[1] - sizePortal.height - 4 : size.y + 4;
76 |
77 | const optionsPosition = {
78 | top: `${top}px`,
79 | left: `${size?.x}px`,
80 | width: `${size?.right - size?.left}px`,
81 | };
82 |
83 | const onFocus = () => {
84 | updateRect();
85 | updatePortalRect();
86 | setOpen(true);
87 | };
88 |
89 | const onBlur = () => {
90 | setOpen(false);
91 | };
92 |
93 | const ref = useRef(null);
94 | useClickOutside(ref, onBlur);
95 |
96 | return (
97 | <>
98 |
106 |
107 |
108 |
109 | {open && (
110 |
111 |
112 | {placeholder && (
113 |
116 | )}
117 | {Object.keys(options).map((k, i) => (
118 |
127 | ))}
128 |
129 |
130 | )}
131 | >
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/src/importer/components/Input/style/Input.module.scss:
--------------------------------------------------------------------------------
1 | @import "./mixins.scss";
2 |
3 | .container {
4 | margin-bottom: var(--m);
5 | text-align: left;
6 |
7 | &:only-child {
8 | margin: var(--m-xxs) 0;
9 | }
10 |
11 | &:has(input[disabled]) {
12 | .label {
13 | cursor: default;
14 | }
15 | }
16 |
17 | & > label {
18 | font: inherit;
19 | color: inherit;
20 | font-weight: inherit;
21 | line-height: inherit;
22 | background-color: transparent;
23 | border: none;
24 | text-align: inherit;
25 | padding: 0;
26 | font-size: inherit;
27 | accent-color: var(--color-primary);
28 | }
29 | }
30 |
31 | .label {
32 | display: inline-block;
33 | margin-bottom: var(--m-xxs);
34 | cursor: pointer;
35 | line-height: 1;
36 | font-weight: 500;
37 | font-family: var(--font-family-1);
38 | overflow: hidden;
39 | white-space: nowrap;
40 | text-overflow: ellipsis;
41 | }
42 |
43 | .inputWrapper {
44 | background-color: var(--color-input-background);
45 | border-radius: var(--border-radius-2);
46 | outline: 1px solid transparent;
47 | padding: calc(var(--m-xxs) / 2);
48 | border: 1px solid var(--color-border);
49 | font-weight: 400;
50 | display: flex;
51 | align-items: center;
52 | min-width: 270px;
53 | position: relative;
54 |
55 | &:focus-within,
56 | &:has(.open) {
57 | outline-color: var(--color-text);
58 | z-index: 11;
59 |
60 | .icon svg:not([data-stroke="no-stroke"]) path {
61 | stroke: var(--color-text);
62 | }
63 |
64 | .options {
65 | display: flex;
66 | }
67 | }
68 |
69 | &:has(> [error]),
70 | &.hasError {
71 | border-color: var(--color-text-error);
72 |
73 | .icon svg:not([data-stroke="no-stroke"]) path {
74 | stroke: var(--color-text-error);
75 | }
76 | }
77 |
78 | &:has(> [disabled]) {
79 | background-color: var(--color-input-disabled);
80 | color: var(--color-input-text-disabled);
81 | border-color: var(--color-border-soft);
82 |
83 | .icon.dropdownIcon {
84 | cursor: default;
85 | }
86 | }
87 |
88 | .icon {
89 | padding: 0 var(--m-xxxs);
90 | cursor: pointer;
91 | display: flex;
92 |
93 | button {
94 | background: none;
95 | padding: 0;
96 | border: none;
97 | cursor: pointer;
98 | }
99 |
100 | svg:not([data-stroke="no-stroke"]) path {
101 | display: block;
102 | stroke: var(--color-text-soft);
103 | }
104 | }
105 |
106 | input,
107 | select,
108 | textarea {
109 | -webkit-tap-highlight-color: transparent;
110 | appearance: none;
111 | resize: none;
112 | width: 100%;
113 | display: block;
114 | padding: calc(var(--m-xxs) / 2);
115 | border: none;
116 | background-color: transparent;
117 | flex-grow: 1;
118 | color: var(--color-text);
119 | font: inherit;
120 | font-weight: inherit;
121 | line-height: inherit;
122 | accent-color: var(--color-primary);
123 |
124 | @include placeholder {
125 | color: var(--color-text-soft);
126 | }
127 |
128 | option:disabled {
129 | color: var(--color-text-soft);
130 | }
131 | option:not([value]) {
132 | display: none;
133 | }
134 |
135 | &:focus {
136 | outline: none;
137 | }
138 | }
139 |
140 | select option {
141 | background-color: var(--color-input-background);
142 | border: none;
143 | padding: var(--m);
144 | }
145 |
146 | .fluid & {
147 | min-width: auto;
148 | }
149 |
150 | .small & {
151 | padding: 0 var(--m-xxxs);
152 | min-width: auto;
153 | border-radius: var(--border-radius-1);
154 |
155 | .options {
156 | border-radius: var(--border-radius-1);
157 |
158 | .option {
159 | border-radius: var(--border-radius-1);
160 | }
161 | }
162 | }
163 |
164 | .optionsRef {
165 | display: flex;
166 | position: absolute;
167 | top: 100%;
168 | right: 0;
169 | left: 0;
170 | }
171 |
172 | .select:not([disabled]) {
173 | cursor: pointer;
174 | }
175 | }
176 |
177 | .options {
178 | position: fixed;
179 | top: 100%;
180 | right: 0;
181 | left: 0;
182 | z-index: 1000;
183 |
184 | .inner {
185 | display: flex;
186 | background-color: var(--color-input-background);
187 | border-radius: var(--border-radius-2);
188 | padding: var(--m-xxxxs);
189 | outline: 1px solid var(--color-border);
190 | display: flex;
191 | flex-direction: column;
192 | gap: var(--m-xxxxs);
193 | max-height: calc(100vh - 8px);
194 | overflow-y: auto;
195 | margin-top: var(--m-xxxxs);
196 | width: 100%;
197 | }
198 |
199 | .option {
200 | padding: var(--m-xxs) var(--m-xxs);
201 | border-radius: var(--border-radius-2);
202 | background-color: transparent;
203 | border: none;
204 |
205 | &[disabled] {
206 | color: var(--color-text-soft);
207 | }
208 |
209 | &:not([disabled]) {
210 | cursor: pointer;
211 |
212 | &:hover,
213 | &:focus,
214 | &.selected {
215 | background-color: var(--color-background-menu-hover);
216 | }
217 | &.placeholder {
218 | color: var(--color-text-soft);
219 | }
220 | }
221 | .requiredMark {
222 | color: var(--color-text-error);
223 | }
224 | }
225 | }
226 |
227 | .error {
228 | color: var(--color-text-error);
229 | margin-top: var(--m-xxs);
230 | line-height: 1;
231 | font-weight: 400;
232 | }
233 |
234 | .footer {
235 | color: var(--color-text-soft);
236 | margin-top: var(--m-xxs);
237 | line-height: 1;
238 | font-weight: 400;
239 | }
240 |
--------------------------------------------------------------------------------
/src/importer/components/Input/style/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin optional-at-root($sel) {
2 | @at-root #{if(not &, $sel, selector-append(&, $sel))} {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin placeholder {
8 | @include optional-at-root("::-webkit-input-placeholder") {
9 | @content;
10 | }
11 |
12 | @include optional-at-root(":-moz-placeholder") {
13 | @content;
14 | }
15 |
16 | @include optional-at-root("::-moz-placeholder") {
17 | @content;
18 | }
19 |
20 | @include optional-at-root(":-ms-input-placeholder") {
21 | @content;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/importer/components/Input/types/index.ts:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, InputHTMLAttributes, ReactElement } from "react";
2 |
3 | export type inputTypes =
4 | | "date"
5 | | "datetime-local"
6 | | "email"
7 | | "file"
8 | | "month"
9 | | "number"
10 | | "password"
11 | | "search"
12 | | "tel"
13 | | "text"
14 | | "time"
15 | | "url"
16 | | "week";
17 |
18 | export type InputVariants = "fluid" | "small";
19 | export type InputOption = ButtonHTMLAttributes & { required?: boolean };
20 |
21 | export type InputProps = InputHTMLAttributes &
22 | InputHTMLAttributes &
23 | InputHTMLAttributes & {
24 | as?: "input" | "textarea";
25 | label?: string | ReactElement;
26 | icon?: ReactElement;
27 | iconAfter?: ReactElement;
28 | error?: string;
29 | options?: { [key: string]: InputOption };
30 | variants?: InputVariants[];
31 | type?: inputTypes;
32 | };
33 |
--------------------------------------------------------------------------------
/src/importer/components/Portal/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactPortal, useEffect, useState } from "react";
2 | import { createPortal } from "react-dom";
3 | import { PortalProps } from "./types";
4 |
5 | export default function Portal({ children, className = "root-portal", el = "div" }: PortalProps): ReactPortal {
6 | const [container] = useState(() => {
7 | // This will be executed only on the initial render
8 | // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
9 | return document.createElement(el);
10 | });
11 |
12 | useEffect(() => {
13 | container.classList.add(className);
14 | container.setAttribute("role", "complementary");
15 | container.setAttribute("aria-label", "Notifications");
16 | document.body.appendChild(container);
17 | return () => {
18 | document.body.removeChild(container);
19 | };
20 | }, []);
21 |
22 | return createPortal(children, container);
23 | }
24 |
--------------------------------------------------------------------------------
/src/importer/components/Portal/types/index.ts:
--------------------------------------------------------------------------------
1 | export type PortalProps = React.PropsWithChildren<{
2 | className?: string;
3 | el?: string;
4 | }>;
5 |
--------------------------------------------------------------------------------
/src/importer/components/Stepper/hooks/useStepper.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from "react";
2 | import { Step, StepperProps } from "../types";
3 |
4 | export default function useStepper(steps: Step[], initialStep = 0, skipHeader: boolean): StepperProps {
5 | const [current, setCurrent] = useState(initialStep);
6 |
7 | const step = useMemo(() => steps[current], [current, steps]);
8 |
9 | return { steps, current, step, setCurrent, skipHeader };
10 | }
11 |
--------------------------------------------------------------------------------
/src/importer/components/Stepper/index.tsx:
--------------------------------------------------------------------------------
1 | import { colors } from "../../settings/theme";
2 | import classes from "../../utils/classes";
3 | import { StepperProps } from "./types";
4 | import style from "./style/Stepper.module.scss";
5 | import { PiCheckBold } from "react-icons/pi";
6 |
7 | export default function Stepper({ steps, current, clickable, setCurrent, skipHeader }: StepperProps) {
8 | return (
9 |
10 | {steps.map((step, i) => {
11 | if (step.disabled) return null;
12 | const done = i < current;
13 |
14 | const Element = clickable ? "button" : "div";
15 |
16 | const buttonProps: any = clickable
17 | ? {
18 | onClick: () => setCurrent(i),
19 | type: "button",
20 | }
21 | : {};
22 |
23 | let displayNumber = i + 1;
24 | if (skipHeader && displayNumber > 1) {
25 | displayNumber--;
26 | }
27 |
28 | return (
29 |
33 |
34 | {step.label}
35 |
36 | );
37 | })}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/importer/components/Stepper/style/Stepper.module.scss:
--------------------------------------------------------------------------------
1 | $transition: all 0.3s ease-out;
2 |
3 | .stepper {
4 | display: flex;
5 | gap: var(--m);
6 | margin: var(--m-xs) auto;
7 | justify-content: center;
8 |
9 | .step {
10 | display: flex;
11 | gap: var(--m-xxs);
12 | align-items: center;
13 | transition: $transition;
14 |
15 | .badge {
16 | border-radius: 50%;
17 | border: 1px solid var(--color-border);
18 | aspect-ratio: 1;
19 | width: 2em;
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | transition: $transition;
24 | }
25 |
26 | &.active {
27 | color: var(--color-primary);
28 |
29 | .badge {
30 | background-color: var(--color-primary);
31 | color: var(--color-text-on-primary);
32 | border: none;
33 | }
34 | }
35 | &.done {
36 | .badge {
37 | border-color: var(--color-primary);
38 | }
39 | }
40 |
41 | &:not(:first-of-type) {
42 | &:before {
43 | content: "";
44 | height: 1px;
45 | width: calc(min(140px, 4vw));
46 | background-color: var(--color-border);
47 | border-radius: 2px;
48 | margin-right: var(--m-xs);
49 | }
50 | }
51 | }
52 |
53 | .stepWide {
54 | &:not(:first-of-type) {
55 | &:before {
56 | width: calc(min(120px, 10vw));
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/importer/components/Stepper/types/index.ts:
--------------------------------------------------------------------------------
1 | export type Step = {
2 | label: string;
3 | id?: string | number;
4 | disabled?: boolean;
5 | };
6 |
7 | export type StepperProps = {
8 | steps: Step[];
9 | current: number;
10 | setCurrent: (step: number) => void;
11 | step: Step;
12 | clickable?: boolean;
13 | skipHeader: boolean;
14 | };
15 |
--------------------------------------------------------------------------------
/src/importer/components/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from "react";
2 | import classes from "../../utils/classes";
3 | import { CellProps, RowProps, TableProps } from "./types";
4 | import themeDefault from "./style/Default.module.scss";
5 | import Tooltip from "../Tooltip";
6 |
7 | const TableContext = createContext({});
8 |
9 | export default function Table({
10 | data, // An array of objects with the data to be displayed
11 | keyAsId = "id", // Has to be a unique property in the data array to be used as key
12 | theme, // A CSS module object to style the table
13 | mergeThemes, // Should 'theme' be the only style applied (false) or be merged with the default style (true)
14 | highlightColumns, // Columns that should use the highlighted style
15 | hideColumns = ["id"], // Array of columns to be hidden in the display
16 | emptyState,
17 | heading,
18 | background = "zebra",
19 | columnWidths = [],
20 | columnAlignments = [],
21 | fixHeader = false,
22 | onRowClick,
23 | }: TableProps): React.ReactElement {
24 | // THEME
25 | // Tables receive a full CSS module as theme or applies default styles
26 | // depending on mergeThemes being true it will merge both themes or use only the custom one
27 | // use a copy of ./style/Default.module.scss as base to make a custom theme
28 | // another example of the theme lives in src/features/contents/versions/style/TableTheme.module.scss
29 |
30 | const style = !theme ? themeDefault : mergeThemes ? { ...themeDefault, ...theme } : theme;
31 |
32 | // TABLE HEADINGS
33 | // Hide column title if the item has an action (action button) or the title starts with underscore
34 | const modelDatum = data?.[0];
35 | const thead: any = modelDatum
36 | ? Object.keys(modelDatum).map((k) => {
37 | const value = modelDatum[k];
38 |
39 | if (k.indexOf("_") === 0) {
40 | return "";
41 | }
42 |
43 | if (typeof value === "object" && value?.captionInfo) {
44 | return { key: k, captionInfo: value.captionInfo };
45 | }
46 |
47 | return k;
48 | })
49 | : {};
50 | const context = {
51 | style,
52 | highlightColumns,
53 | hideColumns,
54 | columnWidths,
55 | columnAlignments,
56 | };
57 |
58 | if (!data || !data?.length) return {emptyState || null}
;
59 |
60 | const tableStyle = classes([style?.table, style?.[background], fixHeader && style?.fixHeader]);
61 |
62 | const headingContent = heading ? (
63 | {heading}
64 | ) : (
65 |
66 |
67 |
68 | );
69 |
70 | return (
71 |
72 |
73 | {headingContent}
74 |
75 | {data.map((d, i) => {
76 | const key = keyAsId && d?.[keyAsId] ? d[keyAsId] : i;
77 | const props = { datum: d, onClick: onRowClick };
78 | return
;
79 | })}
80 |
81 |
82 | {!data.length && (
83 |
86 | )}
87 |
88 | );
89 | }
90 |
91 | const Row = ({ datum, onClick, isHeading }: RowProps) => {
92 | const { style, highlightColumns, hideColumns, columnWidths, columnAlignments } = useContext(TableContext);
93 |
94 | const className = classes([style?.tr]);
95 | return (
96 | onClick?.(datum)}>
97 | {Object.keys(datum)
98 | .filter((k) => !hideColumns.includes(datum[k]) && !hideColumns.includes(k))
99 | .map((k, i) => {
100 | // datum is the row
101 | // datum[k] is the content for the cell
102 | // If it is an object with the 'content' property, use that as content (can be JSX or a primitive)
103 | // Another 'raw' property with a primitive value is used to sort and search
104 | let content = (datum[k] as any)?.content || datum[k];
105 | const tooltip = (datum[k] as any)?.tooltip;
106 | const captionInfo = isHeading ? (datum[k] as any)?.captionInfo : null;
107 | const headingKey = isHeading ? (datum[k] as any)?.key : null;
108 | content = isHeading && captionInfo ? {headingKey} : content;
109 | const wrappedContent = content && typeof content === "string" ? {content} : content;
110 |
111 | const cellClass = classes([
112 | highlightColumns?.includes(k) && style.highlight,
113 | !wrappedContent && style.empty,
114 | typeof content !== "string" && style.element,
115 | ]);
116 |
117 | const cellStyle = { width: columnWidths?.[i] || "auto", textAlign: columnAlignments?.[i] || "left" };
118 |
119 | return (
120 |
121 | {wrappedContent}
122 | |
123 | );
124 | })}
125 |
126 | );
127 | };
128 |
129 | const Cell = ({ children, cellClass, cellStyle, tooltip }: CellProps) => {
130 | const { style } = useContext(TableContext);
131 | const cellProps = {
132 | className: classes([style?.td, cellClass, !children && style?.empty]),
133 | role: "cell",
134 | style: cellStyle,
135 | ...(tooltip ? { title: tooltip } : {}),
136 | };
137 | return {children}
;
138 | };
139 |
--------------------------------------------------------------------------------
/src/importer/components/Table/storyData.ts:
--------------------------------------------------------------------------------
1 | const storyData = [
2 | {
3 | id: 1,
4 | Name: {
5 | raw: "John Doe",
6 | content: "John Doe",
7 | captionInfo: "This is a caption example",
8 | },
9 | Age: 25,
10 | Email: "john.doe@example.com",
11 | },
12 | {
13 | id: 2,
14 | Name: {
15 | raw: "Huge line",
16 | content:
17 | "Huge line with overflow. Lorem ipsum dolor sit amet. Aequam memento rebus in arduis servare mentem. Ubi fini saeculi fortunae comutatione supersunt in elipse est.",
18 | tooltip:
19 | "Huge line with overflow. Lorem ipsum dolor sit amet. Aequam memento rebus in arduis servare mentem. Ubi fini saeculi fortunae comutatione supersunt in elipse est.",
20 | },
21 | Age: 30,
22 | Email: "jane.smith@example.com",
23 | },
24 | {
25 | id: 3,
26 | Name: "Mike Johnson",
27 | Age: 28,
28 | Email: "mike.johnson@example.com",
29 | },
30 | {
31 | id: 4,
32 | Name: "Emily Davis",
33 | Age: 32,
34 | Email: "emily.davis@example.com",
35 | },
36 | {
37 | id: 5,
38 | Name: "Alex Wilson",
39 | Age: 27,
40 | Email: "alex.wilson@example.com",
41 | },
42 | {
43 | id: 6,
44 | Name: "Sarah Thompson",
45 | Age: 29,
46 | Email: "sarah.thompson@example.com",
47 | },
48 | {
49 | id: 7,
50 | Name: "Daniel Anderson",
51 | Age: 31,
52 | Email: "daniel.anderson@example.com",
53 | },
54 | {
55 | id: 8,
56 | Name: "Michelle Brown",
57 | Age: 26,
58 | Email: "michelle.brown@example.com",
59 | },
60 | {
61 | id: 9,
62 | Name: "Robert Taylor",
63 | Age: 33,
64 | Email: "robert.taylor@example.com",
65 | },
66 | {
67 | id: 10,
68 | Name: "Laura Miller",
69 | Age: 28,
70 | Email: "laura.miller@example.com",
71 | },
72 | {
73 | id: 11,
74 | Name: "Michael Johnson",
75 | Age: 35,
76 | email: "michael.johnson@example.com",
77 | },
78 | {
79 | id: 12,
80 | Name: "Jessica Davis",
81 | Age: 27,
82 | email: "jessica.davis@example.com",
83 | },
84 | {
85 | id: 13,
86 | Name: "Andrew Smith",
87 | Age: 32,
88 | email: "andrew.smith@example.com",
89 | },
90 | {
91 | id: 14,
92 | Name: "Emily Wilson",
93 | Age: 29,
94 | email: "emily.wilson@example.com",
95 | },
96 | {
97 | id: 15,
98 | Name: "David Anderson",
99 | Age: 33,
100 | email: "david.anderson@example.com",
101 | },
102 | {
103 | id: 16,
104 | Name: "Sophia Brown",
105 | Age: 28,
106 | email: "sophia.brown@example.com",
107 | },
108 | {
109 | id: 17,
110 | Name: "Matthew Taylor",
111 | Age: 31,
112 | email: "matthew.taylor@example.com",
113 | },
114 | {
115 | id: 18,
116 | Name: "Olivia Johnson",
117 | Age: 26,
118 | email: "olivia.johnson@example.com",
119 | },
120 | {
121 | id: 19,
122 | Name: "James Davis",
123 | Age: 30,
124 | email: "james.davis@example.com",
125 | },
126 | {
127 | id: 20,
128 | Name: "Grace Smith",
129 | Age: 27,
130 | email: "grace.smith@example.com",
131 | },
132 | ];
133 |
134 | export default storyData;
135 |
--------------------------------------------------------------------------------
/src/importer/components/Table/style/Default.module.scss:
--------------------------------------------------------------------------------
1 | $radius: var(--border-radius-2);
2 | $cellHeight: 44px;
3 |
4 | .table {
5 | display: flex;
6 | flex-direction: column;
7 | flex: 1 1 auto;
8 | width: 100%;
9 | border-collapse: collapse;
10 | border-spacing: 0;
11 | border-radius: $radius;
12 | outline: 1px solid var(--color-border);
13 | table-layout: fixed;
14 | overflow: hidden;
15 |
16 | .thead {
17 | display: table-header-group;
18 | }
19 |
20 | .tbody {
21 | display: block;
22 | overflow: auto;
23 | width: 100%;
24 | }
25 |
26 | .tr {
27 | display: flex;
28 | width: 100%;
29 | overflow: hidden;
30 | flex-wrap: nowrap;
31 | }
32 |
33 | .td {
34 | display: inline-flex;
35 | align-items: center;
36 | height: $cellHeight;
37 | flex-shrink: 0;
38 | }
39 |
40 | .caption {
41 | display: table-caption;
42 | border-bottom: 1px solid var(--color-border);
43 | background-color: var(--color-background-modal);
44 | padding: 0 var(--m-s);
45 | border-radius: var(--border-radius-2) var(--border-radius-2) 0 0;
46 | }
47 |
48 | .thead {
49 | .tr {
50 | .td {
51 | font-weight: 400;
52 | white-space: nowrap;
53 | padding: var(--m-xxxs) var(--m-s);
54 | border-bottom: 1px solid var(--color-border);
55 | color: var(--color-text-secondary);
56 |
57 | &:first-of-type {
58 | border-radius: $radius 0 0 0;
59 | }
60 |
61 | &:last-child {
62 | border-radius: 0 $radius 0 0;
63 | }
64 | }
65 | }
66 | }
67 |
68 | .tbody {
69 | .tr {
70 | .td {
71 | vertical-align: middle;
72 | padding: var(--m-xxxs) var(--m-s);
73 | font-weight: 400;
74 |
75 | & > span,
76 | & > small {
77 | text-overflow: ellipsis;
78 | white-space: nowrap;
79 | display: block;
80 | overflow: hidden;
81 | }
82 |
83 | &.highlight {
84 | font-weight: 500;
85 | }
86 |
87 | &.element {
88 | padding: 0 var(--m-s);
89 | }
90 | }
91 |
92 | &:hover {
93 | box-shadow: 0 0 0 2px var(--color-border);
94 | position: relative;
95 | }
96 |
97 | &:last-child {
98 | border-radius: 0 0 $radius $radius;
99 |
100 | .td {
101 | border-bottom-color: transparent;
102 |
103 | &:first-of-type {
104 | border-radius: 0 0 0 $radius;
105 | }
106 |
107 | &:last-child {
108 | border-radius: 0 0 $radius 0;
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
115 | &.zebra {
116 | background-color: var(--color-background-modal);
117 |
118 | .tbody {
119 | .tr {
120 | &:nth-child(odd) {
121 | .td {
122 | background-color: var(--color-background);
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
129 | &.dark {
130 | background-color: var(--color-background);
131 |
132 | .thead .tr .td {
133 | background-color: var(--color-background-modal);
134 | }
135 | }
136 |
137 | &.light {
138 | background-color: var(--color-background-modal);
139 | }
140 |
141 | &.transparent {
142 | .thead {
143 | background-color: var(--color-background-modal);
144 | }
145 | .tbody {
146 | .tr {
147 | &:hover {
148 | box-shadow: none;
149 | position: static;
150 | }
151 | }
152 | }
153 | }
154 |
155 | &.dark,
156 | &.light {
157 | .tbody {
158 | .tr {
159 | &:hover {
160 | box-shadow: none;
161 | position: static;
162 | }
163 |
164 | &:first-of-type .td {
165 | padding-top: var(--m-s);
166 | height: calc(#{$cellHeight} + var(--m-s) - var(--m-xxxs));
167 | }
168 |
169 | &:last-of-type .td {
170 | padding-bottom: var(--m-s);
171 | height: calc(#{$cellHeight} + var(--m-s) - var(--m-xxxs));
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
178 | .emptyMsg {
179 | display: flex;
180 | justify-content: center;
181 | align-items: center;
182 | margin-top: 15vh;
183 | text-align: center;
184 | }
185 |
--------------------------------------------------------------------------------
/src/importer/components/Table/types/index.ts:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, ReactElement } from "react";
2 |
3 | type Style = { readonly [key: string]: string };
4 |
5 | type Primitive = string | number | boolean | null | undefined;
6 |
7 | export type TableComposite = {
8 | raw: Primitive;
9 | content: Primitive | React.ReactElement;
10 | tooltip?: string;
11 | captionInfo?: string;
12 | };
13 |
14 | export type TableValue = Primitive | TableComposite;
15 |
16 | export type TableDatum = {
17 | [key: string]: TableValue;
18 | };
19 |
20 | export type TableData = TableDatum[];
21 |
22 | export type TableProps = {
23 | data: TableData;
24 | keyAsId?: string;
25 | theme?: Style;
26 | mergeThemes?: boolean;
27 | highlightColumns?: string[];
28 | hideColumns?: string[];
29 | emptyState?: ReactElement;
30 | heading?: ReactElement;
31 | background?: "zebra" | "dark" | "light" | "transparent";
32 | columnWidths?: string[];
33 | columnAlignments?: ("left" | "center" | "right" | "")[];
34 | fixHeader?: boolean;
35 | onRowClick?: (row: TableDatum) => void;
36 | };
37 |
38 | export type RowProps = {
39 | datum: TableDatum;
40 | isHeading?: boolean;
41 | onClick?: (row: TableDatum) => void;
42 | };
43 |
44 | export type CellProps = PropsWithChildren<{
45 | cellClass?: string;
46 | cellStyle: Style;
47 | tooltip?: string;
48 | }>;
49 |
--------------------------------------------------------------------------------
/src/importer/components/ToggleFilter/ToggleFilter.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ToggleFilter from './index';
3 |
4 | export default {
5 | title: 'User Interface/ToggleFilter',
6 | component: ToggleFilter,
7 | argTypes: {
8 | options: {
9 | control: {
10 | type: 'object'
11 | }
12 | },
13 | onChange: { action: 'changed' }
14 | }
15 | };
16 |
17 | export const Default = () => (
18 | console.log(option)}
25 | />
26 | );
27 |
28 | export const WithCustomOptions = () => (
29 | console.log(option)}
35 | />
36 | );
37 |
--------------------------------------------------------------------------------
/src/importer/components/ToggleFilter/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import classes from "../../utils/classes";
3 | import { Option, ToggleFilterProps } from "./types";
4 | import style from "./style/ToggleFilter.module.scss";
5 |
6 | function ToggleFilter({ options, onChange, className }: ToggleFilterProps) {
7 | const [selectedOption, setSelectedOption] = useState(null);
8 | const toggleFilterClassName = classes([style.toggleFilter, className]);
9 |
10 | useEffect(() => {
11 | const defaultSelected = options.find((option) => option.selected);
12 | setSelectedOption(defaultSelected ? defaultSelected.label : options[0]?.label);
13 | }, [options]);
14 |
15 | const handleClick = (option: Option) => {
16 | setSelectedOption(option.label);
17 | if (onChange) {
18 | onChange(option.filterValue);
19 | }
20 | };
21 |
22 | const getOptionColor = (option: Option) => {
23 | if (option.color) {
24 | return option.color;
25 | }
26 | return selectedOption === option.label ? "var(--color-tertiary)" : "var(--color-text)";
27 | };
28 |
29 | return (
30 |
31 | {options.map((option) => (
32 |
43 | ))}
44 |
45 | );
46 | }
47 |
48 | export default ToggleFilter;
49 |
--------------------------------------------------------------------------------
/src/importer/components/ToggleFilter/style/ToggleFilter.module.scss:
--------------------------------------------------------------------------------
1 | .toggleFilter {
2 | display: flex;
3 | align-items: center;
4 | background-color: var(--color-input-background);
5 | border-radius: 20px;
6 | overflow: hidden;
7 | min-height: 36px;
8 | }
9 |
10 | .toggleOption {
11 | padding: 8px 16px;
12 | cursor: pointer;
13 |
14 | &.selected {
15 | background-color: var(--color-text-on-tertiary);
16 | border-radius: 20px;
17 | transition: background-color 0.2s, color 0.2s;
18 | }
19 |
20 | .defaultColor {
21 | color: var(--color-text);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/importer/components/ToggleFilter/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface Option {
2 | label: string;
3 | filterValue: string,
4 | selected: boolean;
5 | color?: string;
6 | }
7 |
8 | export interface ToggleFilterProps {
9 | options: Option[];
10 | className?: string;
11 | onChange: (option: string) => void;
12 | }
--------------------------------------------------------------------------------
/src/importer/components/Tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import ReactDOM from "react-dom";
3 | import classes from "../../utils/classes";
4 | import getStringLengthOfChildren from "../../utils/getStringLengthOfChildren";
5 | import { AsMap, TooltipProps } from "./types";
6 | import style from "./style/Tooltip.module.scss";
7 | import { PiInfo } from "react-icons/pi";
8 |
9 | export default function Tooltip({ as, className, title, children, icon = , ...props }: TooltipProps) {
10 | const Tag: any = as || "span";
11 |
12 | const length = getStringLengthOfChildren(title);
13 | const wrapperClasses = classes([style.tooltip, className, length > 30 && style.multiline]);
14 |
15 | const [tooltipVisible, setTooltipVisible] = useState(false);
16 | const [position, setPosition] = useState({ top: 0, left: 0 });
17 | const targetRef = useRef(null);
18 |
19 | // Create a ref to attach the tooltip portal to
20 | const tooltipContainer = useRef(document.createElement("div"));
21 |
22 | useEffect(() => {
23 | // Appending the tooltip container to the body on mount
24 | document.body.appendChild(tooltipContainer.current);
25 |
26 | // Removing the tooltip container from the body on unmount
27 | return () => {
28 | document.body.removeChild(tooltipContainer.current);
29 | };
30 | }, []);
31 |
32 | const showTooltip = () => {
33 | if (targetRef.current) {
34 | const rect = targetRef.current.getBoundingClientRect();
35 | setPosition({
36 | top: rect.bottom + window.scrollY,
37 | left: rect.left + rect.width / 2 + window.scrollX,
38 | });
39 | setTooltipVisible(true);
40 | }
41 | };
42 |
43 | const hideTooltip = () => {
44 | setTooltipVisible(false);
45 | };
46 |
47 | const tooltipMessage = tooltipVisible && (
48 |
49 | {title}
50 |
51 | );
52 |
53 | return (
54 |
55 | {children}
56 |
57 | {icon}
58 | {tooltipMessage}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/importer/components/Tooltip/style/Tooltip.module.scss:
--------------------------------------------------------------------------------
1 | $side: var(--m-xxxs);
2 | $height: calc($side * 1.732);
3 |
4 | .tooltip {
5 | display: inline-flex;
6 | align-items: center;
7 | gap: var(--m-xs);
8 |
9 | .icon {
10 | position: relative;
11 | display: block;
12 | cursor: pointer;
13 | }
14 |
15 | &.multiline .message {
16 | width: 260px;
17 | white-space: normal;
18 | }
19 | }
20 |
21 | .message {
22 | position: absolute;
23 | transform: translateX(-50%);
24 | background-color: var(--color-background-modal);
25 | z-index: 3;
26 | padding: var(--m-xxs) var(--m-xs);
27 | border-radius: var(--border-radius);
28 | margin-top: var(--m-xs);
29 | box-shadow: 0 0 0 1px var(--color-border), 0 5px 15px rgba(0, 0, 0, 0.2);
30 | max-width: 300px;
31 |
32 | &::after,
33 | &::before {
34 | position: absolute;
35 | top: calc($height * -1);
36 | left: 50%;
37 | border-left: $side solid transparent;
38 | border-right: $side solid transparent;
39 | border-bottom: $height solid var(--color-border);
40 | content: "";
41 | font-size: 0;
42 | line-height: 0;
43 | width: 0;
44 | transform: translateX(-50%);
45 | }
46 |
47 | &::after {
48 | top: calc($height * -1 + 2px);
49 | border-bottom: $height solid var(--color-background-modal);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/importer/components/Tooltip/types/index.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export type AsMap = {
4 | div: React.HTMLProps;
5 | span: React.HTMLProps;
6 | p: React.HTMLProps;
7 | };
8 |
9 | export type TooltipProps = {
10 | as?: T;
11 | title?: string | ReactNode;
12 | icon?: ReactNode;
13 | } & AsMap[T];
14 |
--------------------------------------------------------------------------------
/src/importer/components/UploaderWrapper/UploaderWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useDropzone } from "react-dropzone";
3 | import { useTranslation } from "react-i18next";
4 | import { Button } from "@chakra-ui/button";
5 | import { Box, Text } from "@chakra-ui/react";
6 | import useThemeStore from "../../stores/theme";
7 | import { UploaderWrapperProps } from "./types";
8 | import { PiArrowCounterClockwise, PiFile } from "react-icons/pi";
9 |
10 | export default function UploaderWrapper({ onSuccess, setDataError, ...props }: UploaderWrapperProps) {
11 | const [loading, setLoading] = useState(false);
12 | const theme = useThemeStore((state) => state.theme);
13 | const { t } = useTranslation();
14 |
15 | const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
16 | noClick: true,
17 | noKeyboard: true,
18 | maxFiles: 1,
19 | // maxSize: 1 * Math.pow(1024, 3),
20 | accept: {
21 | "application/vnd.ms-excel": [".xls"],
22 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
23 | "text/csv": [".csv"],
24 | },
25 | onDropRejected: (fileRejections) => {
26 | setLoading(false);
27 | // const errorMessage = fileRejections.map((fileRejection) => fileRejection.errors[0].message).join(", ");
28 | const errorMessage = fileRejections[0].errors[0].message;
29 | setDataError(errorMessage);
30 | },
31 | onDropAccepted: async ([file]) => {
32 | setLoading(true);
33 | onSuccess(file);
34 | setLoading(false);
35 | },
36 | });
37 |
38 | return (
39 |
40 |
51 |
52 | {isDragActive ? (
53 | {t("Drop your file here")}
54 | ) : loading ? (
55 | {t("Loading...")}
56 | ) : (
57 | <>
58 | {t("Drop your file here")}
59 | {t("or")}
60 | }
62 | onClick={open}
63 | mt="6px"
64 | colorScheme={"secondary"}
65 | variant={theme === "light" ? "outline" : "solid"}
66 | _hover={
67 | theme === "light"
68 | ? {
69 | background: "var(--color-border)",
70 | color: "var(--color-text)",
71 | }
72 | : undefined
73 | }>
74 | {t("Browse files")}
75 |
76 | >
77 | )}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/importer/components/UploaderWrapper/style/uppy.overrides.scss:
--------------------------------------------------------------------------------
1 | .uppy-Container.uppy-Container {
2 | flex-grow: 1;
3 |
4 | .uppy {
5 | &-Root {
6 | font-family: var(--font-family-1);
7 | height: 100%;
8 | }
9 |
10 | &-Dashboard {
11 | height: 100%;
12 | &-inner {
13 | background-color: var(--color-background);
14 | border-radius: var(--border-radius-4);
15 | border: 1px solid var(--color-border);
16 | color: var(--color-text-soft);
17 | width: auto !important;
18 | min-width: 200px;
19 | height: 100% !important;
20 | }
21 |
22 | &-AddFiles,
23 | &-dropFilesHereHint {
24 | border: 2px dashed var(--color-border);
25 | border-radius: var(--border-radius-3);
26 | margin: var(--m-s);
27 | color: inherit;
28 | font-size: var(--font-size-xxl);
29 | font-weight: 400;
30 |
31 | &-title {
32 | color: inherit;
33 | font-size: inherit;
34 | }
35 |
36 | &-list:has(span[role="presentation"]:empty) {
37 | display: none;
38 | }
39 | }
40 |
41 | &-browse {
42 | color: var(--importer-link);
43 | }
44 |
45 | &-dropFilesHereHint {
46 | padding: 0;
47 | background-image: none;
48 | inset: 0;
49 | border-color: var(--color-text-soft);
50 | color: var(--color-text);
51 | border-width: 3px;
52 | }
53 |
54 | &-Item {
55 | max-width: inherit;
56 | &-previewInnerWrap {
57 | background-color: var(--color-background) !important;
58 | border-radius: var(--border-radius-1);
59 | height: 100% !important;
60 | &::after {
61 | background-color: transparent;
62 | }
63 | }
64 | &-status {
65 | color: inherit;
66 | }
67 | &-action {
68 | &--remove {
69 | width: auto;
70 | height: auto;
71 |
72 | svg {
73 | width: var(--m);
74 | height: var(--m);
75 | aspect-ratio: 1;
76 |
77 | stroke: none;
78 |
79 | path {
80 | &:nth-of-type(1) {
81 | fill: var(--color-text-soft);
82 | }
83 | &:nth-of-type(2) {
84 | fill: var(--color-background);
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | &-progressIndicator {
92 | width: auto;
93 | height: auto;
94 |
95 | svg {
96 | width: var(--m);
97 | height: var(--m);
98 | aspect-ratio: 1;
99 |
100 | circle {
101 | fill: var(--color-green-ui);
102 | }
103 | polygon {
104 | fill: var(--color-text-on-primary);
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
111 | &-DashboardContent {
112 | &-bar {
113 | background-color: transparent;
114 | border-bottom-color: var(--color-border);
115 | height: auto;
116 | padding: var(--m-s);
117 | display: flex;
118 | justify-content: space-between;
119 | width: auto;
120 |
121 | & > * {
122 | flex: 0 1 auto;
123 |
124 | &:empty {
125 | display: none;
126 | }
127 | }
128 | }
129 | &-title {
130 | all: unset;
131 | }
132 | &-addMore,
133 | &-back,
134 | &-save {
135 | color: var(--color-text);
136 | padding: 0;
137 | font-size: inherit;
138 | display: inline;
139 | margin: 0;
140 |
141 | &:focus {
142 | background-color: transparent;
143 | outline: 2px solid var(--color-text);
144 | outline-offset: var(--m-xxxs);
145 | }
146 | }
147 | }
148 |
149 | &-StatusBar {
150 | border-top-color: transparent;
151 | height: auto;
152 | background-color: transparent;
153 | padding: var(--m-s);
154 |
155 | &[aria-hidden="true"] {
156 | display: none;
157 | }
158 |
159 | &-progress {
160 | left: 0;
161 | top: 0;
162 | background-color: var(--color-green-ui);
163 | }
164 |
165 | &:before {
166 | background-color: var(--color-border);
167 | height: 0;
168 | }
169 |
170 | &-content {
171 | color: var(--color-text-soft);
172 | font-size: inherit;
173 | padding-left: 0;
174 | }
175 |
176 | &-actions {
177 | padding: var(--m-s);
178 | display: flex;
179 | justify-content: flex-end;
180 | background-color: transparent;
181 | right: 0;
182 |
183 | &:empty {
184 | display: none;
185 | }
186 | }
187 |
188 | &-actionBtn {
189 | color: var(--color-text);
190 | border-radius: var(--border-radius-1);
191 | padding: var(--m-xxxs) var(--m-xxs);
192 | border: 1px solid var(--color-border);
193 | background-color: transparent;
194 | margin-right: var(--m-xs);
195 | font-size: inherit;
196 | height: auto;
197 | display: none;
198 | gap: var(--m-xxxs);
199 |
200 | svg {
201 | position: static;
202 | }
203 |
204 | &:not([disabled]):hover {
205 | border-color: var(--color-border-hover);
206 | }
207 |
208 | &--upload {
209 | padding: 0.5px var(--m-s);
210 | border-radius: var(--border-radius-2);
211 | font-size: var(--font-size-xs);
212 | font-weight: 400;
213 | background-color: var(--color-primary);
214 | color: var(--color-text-on-primary);
215 |
216 | &:not([disabled]):not(.disableHover):hover,
217 | &:not([disabled]):not(.disableHover):active {
218 | background-color: var(--color-primary-hover);
219 | color: var(--color-text-on-primary);
220 | }
221 |
222 | &:is([disabled]) {
223 | background-color: var(--color-primary-disabled);
224 | color: var(--color-text-on-primary-disabled);
225 | }
226 | }
227 | }
228 |
229 | &-statusPrimary {
230 | line-height: inherit;
231 | }
232 |
233 | &-statusIndicator {
234 | color: var(--color-green-ui);
235 | }
236 |
237 | &.is-error {
238 | .uppy-StatusBar-statusIndicator {
239 | color: var(--color-red);
240 | }
241 | .uppy-StatusBar-statusPrimary {
242 | color: var(--color-red);
243 | }
244 | }
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/importer/components/UploaderWrapper/types/index.ts:
--------------------------------------------------------------------------------
1 | import { UploaderProps } from "../../../features/uploader/types";
2 |
3 | export type UploaderWrapperProps = Omit & {};
4 |
--------------------------------------------------------------------------------
/src/importer/features/complete/index.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import { Button } from "@chakra-ui/button";
3 | import Box from "../../components/Box";
4 | import { CompleteProps } from "./types";
5 | import style from "./style/Complete.module.scss";
6 | import { PiArrowCounterClockwise, PiCheckBold } from "react-icons/pi";
7 |
8 | export default function Complete({ reload, close, isModal }: CompleteProps) {
9 | const { t } = useTranslation();
10 | return (
11 |
12 | <>
13 |
14 |
15 |
16 | {t("Import Successful")}
17 |
18 | } onClick={reload}>
19 | {t("Upload another file")}
20 |
21 | {isModal && (
22 | } onClick={close}>
23 | {t("Done")}
24 |
25 | )}
26 |
27 | >
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/importer/features/complete/style/Complete.module.scss:
--------------------------------------------------------------------------------
1 | .content.content {
2 | max-width: 1000px;
3 | padding-top: var(--m);
4 | height: 100%;
5 | flex: 1 0 100px;
6 | box-shadow: none;
7 | background-color: transparent;
8 | align-self: center;
9 |
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | font-size: var(--font-size-xl);
14 | flex-direction: column;
15 | gap: var(--m);
16 | text-align: center;
17 | position: relative;
18 |
19 | .icon {
20 | width: 64px;
21 | height: 64px;
22 | isolation: isolate;
23 | position: relative;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 |
28 | &::before {
29 | content: "";
30 | position: absolute;
31 | inset: 0;
32 | border-radius: 50%;
33 | background-color: var(--color-green-ui);
34 | z-index: -1;
35 | }
36 |
37 | svg {
38 | width: 38%;
39 | height: 38%;
40 | object-fit: contain;
41 | color: var(--color-text-on-primary);
42 | }
43 | }
44 |
45 | .actions {
46 | display: flex;
47 | gap: var(--m-l);
48 | align-items: center;
49 | justify-content: center;
50 | margin-top: var(--m-xxl);
51 |
52 | & > * {
53 | flex: 1 0 190px;
54 | }
55 |
56 | button {
57 | width: 50%;
58 | }
59 | }
60 | }
61 |
62 | .spinner {
63 | border: 1px solid var(--color-border);
64 | margin-top: var(--m);
65 | padding: var(--m);
66 | border-radius: var(--border-radius-1);
67 | }
68 |
--------------------------------------------------------------------------------
/src/importer/features/complete/types/index.ts:
--------------------------------------------------------------------------------
1 | export type CompleteProps = {
2 | reload: () => void;
3 | close: () => void;
4 | isModal: boolean;
5 | };
6 |
--------------------------------------------------------------------------------
/src/importer/features/main/hooks/useMutableLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useMutableLocalStorage(key: string, initialValue: any) {
4 | // State to store our value
5 | // Pass initial state function to useState so logic is only executed once
6 | const getLocalStorage = () => {
7 | if (typeof window === "undefined") {
8 | return initialValue;
9 | }
10 | try {
11 | // Get from local storage by key
12 | const item = window.localStorage.getItem(key);
13 | // Parse stored json or if none return initialValue
14 | return item ? JSON.parse(item) : initialValue;
15 | } catch (error) {
16 | // If error also return initialValue
17 | console.log(error);
18 | return initialValue;
19 | }
20 | };
21 | const [storedValue, setStoredValue] = useState(getLocalStorage());
22 |
23 | useEffect(() => {
24 | setStoredValue(getLocalStorage());
25 | }, [key]);
26 |
27 | // Return a wrapped version of useState's setter function that ...
28 | // ... persists the new value to localStorage.
29 | const setValue = (value: any) => {
30 | try {
31 | // Allow value to be a function so we have same API as useState
32 | const valueToStore = value instanceof Function ? value(storedValue) : value;
33 | // Save state
34 | setStoredValue(valueToStore);
35 | // Save to local storage
36 | if (typeof window !== "undefined") {
37 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
38 | }
39 | } catch (error) {
40 | console.log(error);
41 | }
42 | };
43 |
44 | return [storedValue, setValue];
45 | }
46 |
--------------------------------------------------------------------------------
/src/importer/features/main/hooks/useStepNavigation.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useTranslation } from "react-i18next";
3 | import useStepper from "../../../components/Stepper/hooks/useStepper";
4 | import { Steps } from "../types";
5 | import useMutableLocalStorage from "./useMutableLocalStorage";
6 |
7 | export const StepEnum = {
8 | Upload: 0,
9 | RowSelection: 1,
10 | MapColumns: 2,
11 | Complete: 3,
12 | };
13 |
14 | const calculateNextStep = (nextStep: number, skipHeader: boolean) => {
15 | if (skipHeader) {
16 | switch (nextStep) {
17 | case StepEnum.Upload:
18 | case StepEnum.RowSelection:
19 | return StepEnum.MapColumns;
20 | case StepEnum.MapColumns:
21 | return StepEnum.Complete;
22 | default:
23 | return nextStep;
24 | }
25 | }
26 | return nextStep;
27 | };
28 |
29 | const getStepConfig = (skipHeader: boolean) => {
30 | return [
31 | { label: "Upload", id: Steps.Upload },
32 | { label: "Select Header", id: Steps.RowSelection, disabled: skipHeader },
33 | { label: "Map Columns", id: Steps.MapColumns },
34 | ];
35 | };
36 |
37 | function useStepNavigation(initialStep: number, skipHeader: boolean) {
38 | const [t] = useTranslation();
39 | const translatedSteps = getStepConfig(skipHeader).map((step) => ({
40 | ...step,
41 | label: t(step.label),
42 | }));
43 | const stepper = useStepper(translatedSteps, StepEnum.Upload, skipHeader);
44 | const [storageStep, setStorageStep] = useMutableLocalStorage(`tf_steps`, "");
45 | const [currentStep, setCurrentStep] = useState(initialStep);
46 |
47 | const goBack = (backStep = 0) => {
48 | backStep = backStep || currentStep - 1 || 0;
49 | setStep(backStep);
50 | };
51 |
52 | const goNext = (nextStep = 0) => {
53 | nextStep = nextStep || currentStep + 1 || 0;
54 | const calculatedStep = calculateNextStep(nextStep, skipHeader);
55 | setStep(calculatedStep);
56 | };
57 |
58 | const setStep = (newStep: number) => {
59 | setCurrentStep(newStep);
60 | setStorageStep(newStep);
61 | stepper.setCurrent(newStep);
62 | };
63 |
64 | useEffect(() => {
65 | stepper.setCurrent(storageStep || 0);
66 | setCurrentStep(storageStep || 0);
67 | }, [storageStep]);
68 |
69 | return {
70 | currentStep: storageStep || currentStep,
71 | setStep,
72 | goBack,
73 | goNext,
74 | stepper,
75 | stepId: stepper?.step?.id,
76 | setStorageStep,
77 | };
78 | }
79 |
80 | export default useStepNavigation;
81 |
--------------------------------------------------------------------------------
/src/importer/features/main/index.tsx:
--------------------------------------------------------------------------------
1 | import Papa from "papaparse";
2 | import { useEffect, useState } from "react";
3 | import * as XLSX from "xlsx";
4 | import { IconButton } from "@chakra-ui/button";
5 | import Errors from "../../components/Errors";
6 | import Stepper from "../../components/Stepper";
7 | import { CSVImporterProps } from "../../../types";
8 | import useCustomStyles from "../../hooks/useCustomStyles";
9 | import { Template } from "../../types";
10 | import { convertRawTemplate } from "../../utils/template";
11 | import { parseObjectOrStringJSON } from "../../utils/utils";
12 | import { TemplateColumnMapping } from "../map-columns/types";
13 | import useStepNavigation, { StepEnum } from "./hooks/useStepNavigation";
14 | import { FileData, FileRow } from "./types";
15 | import style from "./style/Main.module.scss";
16 | import Complete from "../complete";
17 | import MapColumns from "../map-columns";
18 | import RowSelection from "../row-selection";
19 | import Uploader from "../uploader";
20 | import { PiX } from "react-icons/pi";
21 | import { useTranslation } from "react-i18next";
22 |
23 | export default function Main(props: CSVImporterProps) {
24 | const {
25 | isModal = true,
26 | modalOnCloseTriggered = () => null,
27 | template,
28 | onComplete,
29 | customStyles,
30 | showDownloadTemplateButton,
31 | skipHeaderRowSelection,
32 | } = props;
33 | const skipHeader = skipHeaderRowSelection ?? false;
34 |
35 | const { t } = useTranslation();
36 |
37 | // Apply custom styles
38 | useCustomStyles(parseObjectOrStringJSON("customStyles", customStyles));
39 |
40 | // Stepper handler
41 | const { currentStep, setStep, goNext, goBack, stepper, setStorageStep } = useStepNavigation(StepEnum.Upload, skipHeader);
42 |
43 | // Error handling
44 | const [initializationError, setInitializationError] = useState(null);
45 | const [dataError, setDataError] = useState(null);
46 |
47 | // File data
48 | const emptyData = {
49 | fileName: "",
50 | rows: [],
51 | sheetList: [],
52 | errors: [],
53 | };
54 | const [data, setData] = useState(emptyData);
55 |
56 | // Header row selection state
57 | const [selectedHeaderRow, setSelectedHeaderRow] = useState(0);
58 |
59 | // Map of upload column index -> TemplateColumnMapping
60 | const [columnMapping, setColumnMapping] = useState<{ [index: number]: TemplateColumnMapping }>({});
61 |
62 | // Used in the final step to show a loading indicator while the data is submitting
63 | const [isSubmitting, setIsSubmitting] = useState(false);
64 |
65 | const [parsedTemplate, setParsedTemplate] = useState({
66 | columns: [],
67 | });
68 |
69 | useEffect(() => {
70 | const [parsedTemplate, parsedTemplateError] = convertRawTemplate(template);
71 | if (parsedTemplateError) {
72 | setInitializationError(parsedTemplateError);
73 | } else if (parsedTemplate) {
74 | setParsedTemplate(parsedTemplate);
75 | }
76 | }, [template]);
77 |
78 | useEffect(() => {
79 | // TODO (client-sdk): Have the importer continue where left off if closed
80 | // Temporary solution to reload state if closed and opened again
81 | if (data.rows.length === 0 && currentStep !== StepEnum.Upload) {
82 | reload();
83 | }
84 | }, [data]);
85 |
86 | // Actions
87 | const reload = () => {
88 | setData(emptyData);
89 | setSelectedHeaderRow(0);
90 | setColumnMapping({});
91 | setDataError(null);
92 | setStep(StepEnum.Upload);
93 | };
94 |
95 | const requestClose = () => {
96 | if (!isModal) {
97 | return;
98 | }
99 | modalOnCloseTriggered && modalOnCloseTriggered();
100 | if (currentStep === StepEnum.Complete) {
101 | reload();
102 | }
103 | };
104 |
105 | if (initializationError) {
106 | return (
107 |
108 |
109 |
110 | );
111 | }
112 |
113 | const renderContent = () => {
114 | switch (currentStep) {
115 | case StepEnum.Upload:
116 | return (
117 | {
123 | setDataError(null);
124 | const fileType = file.name.slice(file.name.lastIndexOf(".") + 1);
125 | if (!["csv", "xls", "xlsx"].includes(fileType)) {
126 | setDataError(t("Only CSV, XLS, and XLSX files can be uploaded"));
127 | return;
128 | }
129 | const reader = new FileReader();
130 | const isNotBlankRow = (row: string[]) => row.some((cell) => cell.toString().trim() !== "");
131 | reader.onload = async (e) => {
132 | const bstr = e?.target?.result;
133 | if (!bstr) {
134 | return;
135 | }
136 | switch (fileType) {
137 | case "csv":
138 | Papa.parse(bstr.toString(), {
139 | complete: function (results) {
140 | const csvData = results.data as Array>;
141 | const rows: FileRow[] = csvData.filter(isNotBlankRow).map((row: string[], index: number) => ({ index, values: row }));
142 | setData({
143 | fileName: file.name,
144 | rows: rows,
145 | sheetList: [],
146 | errors: results.errors.map((error) => error.message),
147 | });
148 | goNext();
149 | },
150 | });
151 | break;
152 | case "xlsx":
153 | case "xls":
154 | const workbook = XLSX.read(bstr as string, { type: "binary" });
155 | const sheetList = workbook.SheetNames;
156 | const data = XLSX.utils.sheet_to_json(workbook.Sheets[sheetList[0]], { header: 1 }) as Array>;
157 | const rows: FileRow[] = data.filter(isNotBlankRow).map((row: string[], index: number) => ({ index, values: row }));
158 | setData({
159 | fileName: file.name,
160 | rows: rows,
161 | sheetList: sheetList,
162 | errors: [], // TODO: Handle any parsing errors
163 | });
164 | goNext();
165 | break;
166 | }
167 | };
168 |
169 | switch (fileType) {
170 | case "csv":
171 | reader.readAsText(file, "utf-8");
172 | break;
173 | case "xlsx":
174 | case "xls":
175 | reader.readAsBinaryString(file);
176 | break;
177 | }
178 | }}
179 | />
180 | );
181 | case StepEnum.RowSelection:
182 | return (
183 | goNext()}
187 | selectedHeaderRow={selectedHeaderRow}
188 | setSelectedHeaderRow={setSelectedHeaderRow}
189 | />
190 | );
191 | case StepEnum.MapColumns:
192 | return (
193 | {
200 | setIsSubmitting(true);
201 | setColumnMapping(columnMapping);
202 |
203 | // TODO (client-sdk): Move this type, add other data attributes (i.e. column definitions), and move the data processing to a function
204 | type MappedRow = {
205 | index: number;
206 | values: Record;
207 | };
208 | const startIndex = (selectedHeaderRow || 0) + 1;
209 |
210 | const mappedRows: MappedRow[] = [];
211 | data.rows.slice(startIndex).forEach((row: FileRow) => {
212 | const resultingRow: MappedRow = {
213 | index: row.index - startIndex,
214 | values: {},
215 | };
216 | row.values.forEach((value: string, valueIndex: number) => {
217 | const mapping = columnMapping[valueIndex];
218 | if (mapping && mapping.include) {
219 | resultingRow.values[mapping.key] = value;
220 | }
221 | });
222 | mappedRows.push(resultingRow);
223 | });
224 |
225 | const includedColumns = Object.values(columnMapping).filter(({ include }) => include);
226 |
227 | const onCompleteData = {
228 | num_rows: mappedRows.length,
229 | num_columns: includedColumns.length,
230 | error: null,
231 | // TODO (client-sdk): Either remove "name" or change it to the be the name of the original upload column
232 | columns: includedColumns.map(({ key }) => ({ key, name: key })),
233 | rows: mappedRows,
234 | };
235 |
236 | onComplete && onComplete(onCompleteData);
237 |
238 | setIsSubmitting(false);
239 | goNext();
240 | }}
241 | isSubmitting={isSubmitting}
242 | onCancel={skipHeader ? reload : () => goBack(StepEnum.RowSelection)}
243 | />
244 | );
245 | case StepEnum.Complete:
246 | return ;
247 | default:
248 | return null;
249 | }
250 | };
251 |
252 | return (
253 |
254 |
255 |
256 |
257 |
258 |
{renderContent()}
259 |
260 | {!!dataError && (
261 |
266 | )}
267 |
268 | {isModal &&
} onClick={requestClose} />}
269 |
270 | );
271 | }
272 |
--------------------------------------------------------------------------------
/src/importer/features/main/style/Main.module.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | padding: 20px 8px 8px 8px;
6 | }
7 |
8 | .content {
9 | padding: 20px;
10 | flex: 1;
11 | overflow: hidden;
12 | }
13 |
14 | .status {
15 | display: flex;
16 | align-items: center;
17 | justify-content: space-between;
18 | gap: var(--m);
19 | padding: 0 var(--m-s) var(--m-s) var(--m-s);
20 | }
21 |
22 | .spinner {
23 | border: 1px solid var(--color-border);
24 | margin-top: var(--m);
25 | padding: var(--m);
26 | border-radius: var(--border-radius-1);
27 | position: absolute;
28 | top: 50%;
29 | left: 50%;
30 | transform: translate(-50%, -50%);
31 | }
32 |
33 | $closeSide: calc(var(--m-xl) * 36 / 48);
34 |
35 | .close.close {
36 | position: absolute;
37 | right: var(--m-xs, 0.5rem);
38 | top: var(--m-xs, 0.5rem);
39 | border-radius: 50%;
40 | min-width: $closeSide;
41 | height: $closeSide;
42 | aspect-ratio: 1;
43 | font-size: var(--font-size-xl);
44 | padding: 0;
45 | }
46 |
--------------------------------------------------------------------------------
/src/importer/features/main/types/index.ts:
--------------------------------------------------------------------------------
1 | export enum Steps {
2 | Upload = "upload",
3 | RowSelection = "row-selection",
4 | MapColumns = "map-columns",
5 | }
6 |
7 | export type FileRow = {
8 | index: number;
9 | values: string[];
10 | };
11 |
12 | export type FileData = {
13 | fileName: string;
14 | rows: FileRow[];
15 | sheetList: string[];
16 | errors: string[];
17 | };
18 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/components/DropDownFields.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Input from "../../../components/Input";
3 | import { InputOption } from "../../../components/Input/types";
4 |
5 | type DropdownFieldsProps = {
6 | options: { [key: string]: InputOption };
7 | value: string;
8 | placeholder: string;
9 | onChange: (value: string) => void;
10 | selectedValues: { key: string; selected: boolean | undefined }[];
11 | updateSelectedValues: (updatedValues: { key: string; selected: boolean | undefined }[]) => void;
12 | };
13 |
14 | export default function DropdownFields({ options, value, placeholder, onChange, selectedValues, updateSelectedValues }: DropdownFieldsProps) {
15 | const [selectedOption, setSelectedOption] = useState(value);
16 | const [filteredOptions, setFilteredOptions] = useState<{ [key: string]: InputOption }>({});
17 |
18 | useEffect(() => {
19 | setSelectedOption(value);
20 | }, [selectedValues]);
21 |
22 | useEffect(() => {
23 | filterOptions();
24 | }, [options, selectedValues]);
25 |
26 | const handleInputChange = (event: any) => {
27 | const newValue = event;
28 | const updatedSelectedValues = selectedValues.map((item) => {
29 | if (item.key === selectedOption) {
30 | return { ...item, selected: false };
31 | } else if (item.key === newValue) {
32 | return { ...item, selected: true };
33 | }
34 | return item;
35 | });
36 | setSelectedOption(newValue);
37 | updateSelectedValues([...updatedSelectedValues]);
38 | onChange(newValue);
39 | };
40 |
41 | const filterOptions = () => {
42 | const newFilteredOptions: { [key: string]: InputOption } = {};
43 | for (const key in options) {
44 | const option = options[key];
45 | const isSelected = selectedValues.some((item) => item.key === option?.value && item.selected && option.value !== value);
46 | if (!isSelected) {
47 | newFilteredOptions[key] = option;
48 | }
49 | }
50 | setFilteredOptions(newFilteredOptions);
51 | };
52 |
53 | return (
54 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/hooks/useMapColumnsTable.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from "react";
2 | import { useTranslation } from "react-i18next";
3 | import Checkbox from "../../../components/Checkbox";
4 | import { InputOption } from "../../../components/Input/types";
5 | import DropdownFields from "../components/DropDownFields";
6 | import { TemplateColumn, UploadColumn } from "../../../types";
7 | import stringsSimilarity from "../../../utils/stringSimilarity";
8 | import { TemplateColumnMapping } from "../types";
9 | import style from "../style/MapColumns.module.scss";
10 |
11 | export default function useMapColumnsTable(
12 | uploadColumns: UploadColumn[],
13 | templateColumns: TemplateColumn[] = [],
14 | columnsValues: { [uploadColumnIndex: number]: TemplateColumnMapping },
15 | isLoading?: boolean
16 | ) {
17 | const { t } = useTranslation();
18 | useEffect(() => {
19 | Object.keys(columnsValues).map((uploadColumnIndexStr) => {
20 | const uploadColumnIndex = Number(uploadColumnIndexStr);
21 | const templateKey = columnsValues[uploadColumnIndex].key;
22 | handleTemplateChange(uploadColumnIndex, templateKey);
23 | });
24 | }, []);
25 |
26 | const checkSimilarity = (templateColumnKey: string, uploadColumnName: string) => {
27 | const templateColumnKeyFormatted = templateColumnKey.replace(/_/g, " ");
28 | return stringsSimilarity(templateColumnKeyFormatted, uploadColumnName.toLowerCase()) > 0.9;
29 | };
30 |
31 | const isSuggestedMapping = (templateColumn: TemplateColumn, uploadColumnName: string) => {
32 | if (!templateColumn?.suggested_mappings) {
33 | return false;
34 | }
35 | return templateColumn.suggested_mappings.some((suggestion) => suggestion.toLowerCase() === uploadColumnName.toLowerCase());
36 | };
37 |
38 | const [values, setValues] = useState<{ [key: number]: TemplateColumnMapping }>(() => {
39 | const usedTemplateColumns = new Set();
40 | const initialObject: { [key: number]: TemplateColumnMapping } = {};
41 |
42 | return uploadColumns.reduce((acc, uc) => {
43 | const matchedSuggestedTemplateColumn = templateColumns?.find((tc) => isSuggestedMapping(tc, uc.name));
44 |
45 | if (matchedSuggestedTemplateColumn && matchedSuggestedTemplateColumn.key) {
46 | usedTemplateColumns.add(matchedSuggestedTemplateColumn.key);
47 | acc[uc.index] = { key: matchedSuggestedTemplateColumn.key, include: true };
48 | return acc;
49 | }
50 |
51 | const similarTemplateColumn = templateColumns?.find((tc) => {
52 | if (tc.key && !usedTemplateColumns.has(tc.key) && checkSimilarity(tc.key, uc.name)) {
53 | usedTemplateColumns.add(tc.key);
54 | return true;
55 | }
56 | return false;
57 | });
58 |
59 | acc[uc.index] = {
60 | key: similarTemplateColumn?.key || "",
61 | include: !!similarTemplateColumn?.key,
62 | selected: !!similarTemplateColumn?.key,
63 | };
64 | return acc;
65 | }, initialObject);
66 | });
67 |
68 | const [selectedValues, setSelectedValues] = useState<{ key: string; selected: boolean | undefined }[]>(
69 | Object.values(values).map(({ key, selected }) => ({ key, selected }))
70 | );
71 |
72 | const templateFields: { [key: string]: InputOption } = useMemo(
73 | () => templateColumns.reduce((acc, tc) => ({ ...acc, [tc.name]: { value: tc.key, required: tc.required } }), {}),
74 | [JSON.stringify(templateColumns)]
75 | );
76 |
77 | const handleTemplateChange = (uploadColumnIndex: number, key: string) => {
78 | setValues((prev) => {
79 | const templatesFields = { ...prev, [uploadColumnIndex]: { ...prev[uploadColumnIndex], key: key, include: !!key, selected: !!key } };
80 | const templateFieldsObj = Object.values(templatesFields).map(({ key, selected }) => ({ key, selected }));
81 | setSelectedValues(templateFieldsObj);
82 | return templatesFields;
83 | });
84 | };
85 |
86 | const handleUseChange = (id: number, value: boolean) => {
87 | setValues((prev) => ({ ...prev, [id]: { ...prev[id], include: !!prev[id].key && value } }));
88 | };
89 |
90 | const yourFileColumn = t("Your File Column");
91 | const yourSampleData = t("Your Sample Data");
92 | const destinationColumn = t("Destination Column");
93 | const include = t("Include");
94 |
95 | const rows = useMemo(() => {
96 | return uploadColumns.map((uc, index) => {
97 | const { name, sample_data } = uc;
98 | const suggestion = values?.[index] || {};
99 | const samples = sample_data.filter((d) => d);
100 |
101 | return {
102 | [yourFileColumn]: {
103 | raw: name || false,
104 | content: name || {t("- empty -")},
105 | },
106 | [yourSampleData]: {
107 | raw: "",
108 | content: (
109 |
110 | {samples.map((d, i) => (
111 | {d}
112 | ))}
113 |
114 | ),
115 | },
116 | [destinationColumn]: {
117 | raw: "",
118 | content: (
119 | handleTemplateChange(index, key)}
124 | selectedValues={selectedValues}
125 | updateSelectedValues={setSelectedValues}
126 | />
127 | ),
128 | },
129 | [include]: {
130 | raw: false,
131 | content: (
132 | handleUseChange(index, e.target.checked)}
136 | />
137 | ),
138 | },
139 | };
140 | });
141 | }, [values, isLoading]);
142 | return { rows, formValues: values };
143 | }
144 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/hooks/useNameChange.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useTransformValue = (initialValue: string) => {
4 | const [transformedValue, setTransformedValue] = useState("");
5 |
6 | useEffect(() => {
7 | const keyValue = initialValue.replace(/\s/g, "_").toLowerCase();
8 | setTransformedValue(keyValue);
9 | }, [initialValue]);
10 |
11 | const transformValue = (value: string) => {
12 | const keyValue = value.replace(/\s/g, "_").toLowerCase();
13 | setTransformedValue(keyValue);
14 | };
15 |
16 | return { transformedValue, transformValue };
17 | };
18 |
19 | export default useTransformValue;
20 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useState } from "react";
2 | import { useTranslation } from "react-i18next";
3 | import { Button } from "@chakra-ui/button";
4 | import Errors from "../../components/Errors";
5 | import Table from "../../components/Table";
6 | import { Template, UploadColumn } from "../../types";
7 | import useMapColumnsTable from "./hooks/useMapColumnsTable";
8 | import { MapColumnsProps, TemplateColumnMapping } from "./types";
9 | import style from "./style/MapColumns.module.scss";
10 |
11 | export default function MapColumns({
12 | template,
13 | data,
14 | columnMapping,
15 | selectedHeaderRow,
16 | skipHeaderRowSelection,
17 | onSuccess,
18 | onCancel,
19 | isSubmitting,
20 | }: MapColumnsProps) {
21 | if (data.rows.length === 0) {
22 | return null;
23 | }
24 |
25 | const { t } = useTranslation();
26 | const headerRowIndex = selectedHeaderRow ? selectedHeaderRow : 0;
27 | let sampleDataRows = data.rows.slice(headerRowIndex + 1, headerRowIndex + 4);
28 |
29 | const uploadColumns: UploadColumn[] = data.rows[headerRowIndex]?.values.map((cell, index) => {
30 | let sample_data = sampleDataRows.map((row) => row.values[index]);
31 | return {
32 | index: index,
33 | name: cell,
34 | sample_data,
35 | };
36 | });
37 | const { rows, formValues } = useMapColumnsTable(uploadColumns, template.columns, columnMapping, isSubmitting);
38 | const [error, setError] = useState(null);
39 |
40 | const verifyRequiredColumns = (template: Template, formValues: { [uploadColumnIndex: number]: TemplateColumnMapping }): boolean => {
41 | const requiredColumns = template.columns.filter((column: any) => column.required);
42 | const includedValues = Object.values(formValues).filter((value: any) => value.include);
43 | return requiredColumns.every((requiredColumn: any) => includedValues.some((includedValue: any) => includedValue.key === requiredColumn.key));
44 | };
45 |
46 | const onSubmit = (e: FormEvent) => {
47 | e.preventDefault();
48 | setError(null);
49 |
50 | const columns = Object.entries(formValues).reduce(
51 | (acc, [index, columnMapping]) =>
52 | columnMapping.include
53 | ? {
54 | ...acc,
55 | [index]: columnMapping,
56 | }
57 | : acc,
58 | {}
59 | );
60 |
61 | const isRequiredColumnsIncluded = verifyRequiredColumns(template, formValues);
62 | if (!isRequiredColumnsIncluded) {
63 | setError(t("Please include all required columns"));
64 | return;
65 | }
66 |
67 | onSuccess(columns);
68 | };
69 |
70 | return (
71 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/style/MapColumns.module.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | height: 100%;
3 |
4 | form {
5 | display: flex;
6 | flex-direction: column;
7 | height: 100%;
8 | gap: var(--m);
9 |
10 | .tableWrapper {
11 | display: flex;
12 | height: 100%;
13 | overflow-y: auto;
14 | padding: 1px;
15 | margin-right: -20px;
16 | padding-right: 21px;
17 | }
18 |
19 | .actions {
20 | display: flex;
21 | justify-content: space-between;
22 | }
23 | }
24 | }
25 |
26 | .samples {
27 | overflow: hidden;
28 | text-overflow: ellipsis;
29 | line-height: 1;
30 | white-space: nowrap;
31 |
32 | & > small {
33 | background-color: var(--color-input-background);
34 | font-family: monospace;
35 | padding: var(--m-xxxxs);
36 | border-radius: var(--border-radius-1);
37 | font-size: var(--font-size-xs);
38 | display: inline-block;
39 |
40 | & + small {
41 | margin-left: var(--m-xxxxs);
42 | }
43 | }
44 | }
45 |
46 | .spinner {
47 | border: 1px solid var(--color-border);
48 | margin-top: var(--m);
49 | padding: var(--m);
50 | border-radius: var(--border-radius-1);
51 | }
52 |
53 | .errorContainer {
54 | display: flex;
55 | justify-content: center;
56 | max-width: 60vw;
57 | }
58 |
59 | .schemalessTextInput {
60 | width: 210px;
61 | }
62 |
--------------------------------------------------------------------------------
/src/importer/features/map-columns/types/index.ts:
--------------------------------------------------------------------------------
1 | import { Template } from "../../../types";
2 | import { FileData } from "../../main/types";
3 |
4 | export type TemplateColumnMapping = {
5 | key: string;
6 | include: boolean;
7 | selected?: boolean;
8 | };
9 |
10 | export type MapColumnsProps = {
11 | template: Template;
12 | data: FileData;
13 | columnMapping: { [index: number]: TemplateColumnMapping };
14 | selectedHeaderRow: number | null;
15 | skipHeaderRowSelection?: boolean;
16 | onSuccess: (columnMapping: { [index: number]: TemplateColumnMapping }) => void;
17 | onCancel: () => void;
18 | isSubmitting: boolean;
19 | };
20 |
--------------------------------------------------------------------------------
/src/importer/features/row-selection/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useTranslation } from "react-i18next";
3 | import { Alert } from "@chakra-ui/alert";
4 | import { Button } from "@chakra-ui/button";
5 | import Table from "../../components/Table";
6 | import Tooltip from "../../components/Tooltip";
7 | import { RowSelectionProps } from "./types";
8 | import style from "./style/RowSelection.module.scss";
9 | import { PiWarningCircle } from "react-icons/pi";
10 |
11 | export default function RowSelection({ data, onSuccess, onCancel, selectedHeaderRow, setSelectedHeaderRow }: RowSelectionProps) {
12 | const { t } = useTranslation();
13 | const [isLoading, setIsLoading] = useState(false);
14 | const handleRadioChange = (e: React.ChangeEvent) => {
15 | setSelectedHeaderRow(Number(e.target.value));
16 | };
17 | const rowLimit = 50;
18 |
19 | const dataWithRadios = data.rows.slice(0, rowLimit).map((row) => {
20 | const nameWithRadio = (
21 |
22 |
31 | {row.values?.[0]}
32 |
33 | );
34 | const mappedRow = Object.entries(row.values).map(([key, value]) => {
35 | return [
36 | key,
37 | {
38 | raw: value,
39 | content: key === "0" ? nameWithRadio : {value},
40 | tooltip: value,
41 | },
42 | ];
43 | });
44 | return Object.fromEntries(mappedRow);
45 | });
46 |
47 | const maxNumberOfColumns = 7;
48 | const uploadRow = data.rows[0] ?? { values: {} };
49 | const numberOfColumns = Math.min(Object.keys(uploadRow.values).length, maxNumberOfColumns);
50 | const widthPercentage = 100 / numberOfColumns;
51 | const columnWidths = Array(numberOfColumns).fill(`${widthPercentage}%`);
52 | const hasMultipleExcelSheets = (data.sheetList.length ?? 0) > 1;
53 |
54 | const handleNextClick = (e: any) => {
55 | e.preventDefault();
56 | setIsLoading(true);
57 | onSuccess();
58 | setIsLoading(false);
59 | };
60 |
61 | return (
62 |