diff --git a/packages/web/package.json b/packages/web/package.json index 6b840e870..7f07700bd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -38,6 +38,7 @@ "axios": "^1.2.0", "color2k": "^2.0.0", "cookie": "^0.5.0", + "csv-file-validator": "^2.1.0", "dayjs": "^1.11.7", "diff-match-patch": "^1.0.5", "epubjs": "^0.3.93", diff --git a/packages/web/pages/tools/import/file.tsx b/packages/web/pages/tools/import/file.tsx index 040cdb683..0e212e4c7 100644 --- a/packages/web/pages/tools/import/file.tsx +++ b/packages/web/pages/tools/import/file.tsx @@ -1,28 +1,18 @@ -import { ChangeEvent, useCallback, useMemo, useState } from 'react' -import { Toaster } from 'react-hot-toast' - -import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' -import { applyStoredTheme } from '../../../lib/themeUpdater' - -import { - Box, - HStack, - VStack, -} from '../../../components/elements/LayoutPrimitives' - import 'antd/dist/antd.compact.css' +import CSVFileValidator, { ValidatorConfig } from 'csv-file-validator' +import { ChangeEvent, useState } from 'react' +import { SyncLoader } from 'react-spinners' +import { Button } from '../../../components/elements/Button' +import { FormLabel } from '../../../components/elements/FormElements' +import { HStack, VStack } from '../../../components/elements/LayoutPrimitives' import { StyledText } from '../../../components/elements/StyledText' import { ProfileLayout } from '../../../components/templates/ProfileLayout' +import { theme } from '../../../components/tokens/stitches.config' import { uploadImportFileRequestMutation, UploadImportFileType, } from '../../../lib/networking/mutations/uploadImportFileMutation' -import { Button } from '../../../components/elements/Button' -import { FormLabel } from '../../../components/elements/FormElements' -import { Loader } from '../../../components/templates/SavingRequest' - -import { SyncLoader } from 'react-spinners' -import { theme } from '../../../components/tokens/stitches.config' +import { applyStoredTheme } from '../../../lib/themeUpdater' type UploadState = 'none' | 'uploading' | 'completed' @@ -34,6 +24,71 @@ export default function ImportUploader(): JSX.Element { const [type, setType] = useState() const [uploadState, setUploadState] = useState('none') + const isUrlValid = (url: string | number | boolean) => { + if (typeof url !== 'string') { + return false + } + + try { + new URL(url) + return true + } catch (e) { + return false + } + } + + const isStateValid = (state: string | number | boolean) => { + if (typeof state !== 'string') { + return false + } + + const validStates = ['SUCCEEDED', 'ARCHIVED'] + return validStates.includes(state.toUpperCase()) + } + + const csvConfig: ValidatorConfig = { + headers: [ + { + name: 'url', + inputName: 'url', + required: true, + unique: true, + validate: function (url) { + return isUrlValid(url) + }, + }, + { + name: 'state', + inputName: 'state', + required: false, + optional: true, + validate: function (state) { + return isStateValid(state) + }, + }, + { + name: 'labels', + inputName: 'labels', + required: false, + optional: true, + isArray: true, + }, + { + name: 'saved_at', + inputName: 'saved_at', + required: false, + optional: true, + }, + { + name: 'published_at', + inputName: 'published_at', + required: false, + optional: true, + }, + ], + isHeaderNameOptional: true, + } + const onFinish = (values: unknown) => { console.log(values) } @@ -58,6 +113,23 @@ export default function ImportUploader(): JSX.Element { setUploadState('uploading') try { + if (type == UploadImportFileType.URL_LIST) { + // validate csv file + try { + const csvData = await CSVFileValidator(file, csvConfig) + if (csvData.inValidData.length > 0) { + setErrorMessage(csvData.inValidData[0].message) + setUploadState('none') + return + } + } catch (error) { + console.log(error) + setErrorMessage('Invalid CSV file.') + setUploadState('none') + return + } + } + const result = await uploadImportFileRequestMutation(type, 'text/csv') if (result && result.uploadSignedUrl) { diff --git a/yarn.lock b/yarn.lock index feccfed3f..36a39832d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12734,6 +12734,15 @@ csstype@^3.0.2, csstype@^3.0.4: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== +csv-file-validator@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/csv-file-validator/-/csv-file-validator-2.1.0.tgz#fc83e1e05835d7f03d03f8cce6235938e4cef32e" + integrity sha512-SzBtdw0eZaCIJQYwCsD9uCK6pnbeArS3sJ036kbv56aAyQ7L2v0UmynWgFwclVIPSp74C6ZLd8kxgEHFnhq98w== + dependencies: + famulus "^2.2.3" + lodash "^4.17.21" + papaparse "^5.3.2" + csv-stringify@*, csv-stringify@^6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.4.0.tgz#6d006dca9194700e44f9fbc541bee8bbbd4f459c" @@ -14615,6 +14624,13 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +famulus@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/famulus/-/famulus-2.2.3.tgz#b895c67930d0a0055257e2c1933ab9522dd694a5" + integrity sha512-tEh0NlWBtXSu1t/uY1eN7DQbXXcezPUp2/q25Scbc0h+Wivu9GHcdVnzlOqhD6hetpaj9CMhRm5InSQscM7FWQ== + dependencies: + lodash "^4.17.20" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -22080,7 +22096,7 @@ pako@~1.0.2, pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -papaparse@^5.4.1: +papaparse@^5.3.2, papaparse@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127" integrity sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==