omnivore/packages/web/components/templates/UploadModal.tsx
Thomas Rogers 89e78b377d fix(): Add network timeout to all yarn installs
Bump image-size from 1.2.0 to 1.2.1 (#4567)

Bumps [image-size](https://github.com/image-size/image-size) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/image-size/image-size/releases)
- [Commits](https://github.com/image-size/image-size/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: image-size
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

fix(): Add network timeout to all yarn installs

fix(): remove node-buffer

Bump brace-expansion from 1.1.11 to 1.1.12 in /pkg/admin (#4593)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump @babel/runtime-corejs3 from 7.26.0 to 7.28.2 (#4592)

Bumps [@babel/runtime-corejs3](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs3) from 7.26.0 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-runtime-corejs3)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs3"
  dependency-version: 7.28.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump next from 13.5.8 to 14.2.30 (#4591)

Bumps [next](https://github.com/vercel/next.js) from 13.5.8 to 14.2.30.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.5.8...v14.2.30)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.30
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump axios from 0.27.2 to 0.30.0 (#4590)

Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 0.30.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.30.0/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v0.30.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 0.30.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump @babel/runtime from 7.14.6 to 7.28.2 in /pkg/admin (#4589)

Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.14.6 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump @radix-ui/react-separator from 0.1.4 to 1.1.6 (#4579)

Bumps [@radix-ui/react-separator](https://github.com/radix-ui/primitives) from 0.1.4 to 1.1.6.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump http-proxy-middleware from 2.0.7 to 2.0.9 (#4574)

Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump @babel/helpers from 7.14.6 to 7.28.2 in /pkg/admin (#4594)

Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.14.6 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-version: 7.28.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump pg and @types/pg (#4547)

Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) and [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg). These dependencies needed to be updated together.

Updates `pg` from 8.13.1 to 8.13.3
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.13.3/packages/pg)

Updates `@types/pg` from 8.11.10 to 8.11.11
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/pg"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump @graphql-tools/utils from 9.2.1 to 10.8.6 (#4560)

Bumps [@graphql-tools/utils](https://github.com/ardatan/graphql-tools/tree/HEAD/packages/utils) from 9.2.1 to 10.8.6.
- [Release notes](https://github.com/ardatan/graphql-tools/releases)
- [Changelog](https://github.com/ardatan/graphql-tools/blob/master/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/ardatan/graphql-tools/commits/@graphql-tools/utils@10.8.6/packages/utils)

---
updated-dependencies:
- dependency-name: "@graphql-tools/utils"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

chore(): update versions

chore(): remove unused workload

fix(): kbar dependency

fix(): kbar dependency

chore(): fix packages

fix(): dependencies

fix(): dependencies

fix(): dependencies

fix(): dependencies api

fix(): dependencies api

fix(): dependencies api

chore(): fix order of github

fix(): fix tsconfig for liqe
2025-07-29 19:01:37 +02:00

457 lines
14 KiB
TypeScript

import * as Progress from '@radix-ui/react-progress'
import { styled } from '@stitches/react'
import axios from 'axios'
import { File } from '@phosphor-icons/react'
import { useCallback, useRef, useState } from 'react'
import Dropzone, { DropEvent, DropzoneRef, FileRejection } from 'react-dropzone'
import { v4 as uuidv4 } from 'uuid'
import { uploadFileRequestMutation } from '../../lib/networking/mutations/uploadFileMutation'
import {
uploadImportFileRequestMutation,
UploadImportFileType,
} from '../../lib/networking/mutations/uploadImportFileMutation'
import { showErrorToast } from '../../lib/toastHelpers'
import { validateCsvFile } from '../../utils/csvValidator'
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import {
ModalContent,
ModalOverlay,
ModalRoot,
ModalTitleBar,
} from '../elements/ModalPrimitives'
import { theme } from '../tokens/stitches.config'
const DragnDropContainer = styled('div', {
width: '100%',
height: '80%',
position: 'absolute',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '1',
alignSelf: 'center',
left: 0,
flexDirection: 'column',
padding: '25px',
})
const DragnDropStyle = styled('div', {
border: '1px solid $grayBorder',
borderRadius: '5px',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
color: '$thTextSubtle2',
padding: '10px',
})
const DragnDropIndicator = styled('div', {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: '100%',
height: '100%',
borderRadius: '5px',
})
const ProgressIndicator = styled(Progress.Indicator, {
backgroundColor: '$omnivoreCtaYellow',
width: '100%',
height: '100%',
})
const ProgressRoot = styled(Progress.Root, {
position: 'relative',
overflow: 'hidden',
background: '$omnivoreGray',
borderRadius: '99999px',
width: '100%',
height: '5px',
transform: 'translateZ(0)',
})
type UploadModalProps = {
onOpenChange: (open: boolean) => void
}
type UploadingFile = {
id: string
file: any
name: string
progress: number
status: 'inprogress' | 'success' | 'error'
openUrl: string | undefined
contentType: string
message?: string
}
type UploadInfo = {
uploadSignedUrl?: string
requestId?: string
message?: string
}
export function UploadModal(props: UploadModalProps): JSX.Element {
const [uploadFiles, setUploadFiles] = useState<UploadingFile[]>([
// {
// id: uuidv4(),
// file: '',
// name: 'test file',
// status: 'inprogress',
// progress: (371712 / 864476) * 100,
// openUrl: '',
// },
])
const [inDragOperation, setInDragOperation] = useState(false)
const dropzoneRef = useRef<DropzoneRef | null>(null)
const openDialog = useCallback(
(event: React.MouseEvent) => {
if (dropzoneRef.current) {
dropzoneRef.current.open()
}
event?.preventDefault()
},
[dropzoneRef]
)
const uploadSignedUrlForFile = async (
file: UploadingFile
): Promise<UploadInfo> => {
let { contentType } = file
if (
contentType == 'application/vnd.ms-excel' &&
file.name.endsWith('.csv')
) {
contentType = 'text/csv'
}
switch (contentType) {
case 'text/csv': {
let urlCount = 0
try {
const csvData = await validateCsvFile(file.file)
urlCount = csvData.data.length
if (urlCount > 5000) {
return {
message:
'Due to an increase in traffic we are limiting CSV imports to 5000 items.',
}
}
if (csvData.inValidData.length > 0) {
return {
message: csvData.inValidData[0].message,
}
}
if (urlCount === 0) {
return {
message: 'No URLs found in CSV file.',
}
}
} catch (error) {
return {
message: 'Invalid CSV file.',
}
}
try {
const result = await uploadImportFileRequestMutation(
UploadImportFileType.URL_LIST,
contentType
)
return {
uploadSignedUrl: result?.uploadSignedUrl,
message: `Importing ${urlCount} URLs`,
}
} catch (error) {
console.log('caught error', error)
if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') {
return {
message: 'You have exceeded your maximum daily upload limit.',
}
}
}
}
case 'application/zip': {
const result = await uploadImportFileRequestMutation(
UploadImportFileType.MATTER,
contentType
)
return {
uploadSignedUrl: result?.uploadSignedUrl,
}
}
case 'application/pdf':
case 'application/epub+zip': {
const request = await uploadFileRequestMutation({
// This will tell the backend not to save the URL
// and give it the local filename as the title.
url: `file://local/${file.id}/${file.file.path}`,
contentType: contentType,
createPageEntry: true,
})
return {
uploadSignedUrl: request?.uploadSignedUrl,
requestId: request?.createdPageId,
}
}
}
return {
message: `Invalid content type: ${contentType}`,
}
}
const handleAcceptedFiles = useCallback(
(acceptedFiles: any, event: DropEvent) => {
setInDragOperation(false)
const addedFiles = acceptedFiles.map(
(file: { name: any; type: string }) => {
return {
id: uuidv4(),
file: file,
name: file.name,
progress: 0,
status: 'inprogress',
contentType: file.type,
}
}
)
const allFiles = [...uploadFiles, ...addedFiles]
setUploadFiles(allFiles)
;(async () => {
for (const file of addedFiles) {
try {
const uploadInfo = await uploadSignedUrlForFile(file)
if (!uploadInfo.uploadSignedUrl) {
const message = uploadInfo.message || 'No upload URL available'
showErrorToast(message, { duration: 10000 })
file.status = 'error'
setUploadFiles([...allFiles])
return
}
const uploadResult = await axios.request({
method: 'PUT',
url: uploadInfo.uploadSignedUrl,
data: file.file,
withCredentials: false,
headers: {
'Content-Type': file.file.type,
},
onUploadProgress: (p) => {
if (!p.total) {
console.warn('No total available for upload progress')
return
}
const progress = (p.loaded / p.total) * 100
file.progress = progress
setUploadFiles([...allFiles])
},
})
file.progress = 100
file.status = 'success'
file.openUrl = uploadInfo.requestId
? `/article/sr/${uploadInfo.requestId}`
: undefined
file.message = uploadInfo.message
setUploadFiles([...allFiles])
} catch (error) {
file.status = 'error'
setUploadFiles([...allFiles])
}
}
})()
},
[uploadFiles]
)
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{
bg: '$grayBg',
px: '24px',
minWidth: '650px',
minHeight: '430px',
}}
onInteractOutside={(event) => {
event.preventDefault()
}}
>
<VStack distribution="start">
<ModalTitleBar
title="Upload file"
onOpenChange={props.onOpenChange}
/>
<Dropzone
ref={dropzoneRef}
onDragEnter={() => {
setInDragOperation(true)
}}
onDragLeave={() => {
setInDragOperation(false)
}}
onDropAccepted={handleAcceptedFiles}
onDropRejected={(
fileRejections: FileRejection[],
event: DropEvent
) => {
console.log('onDropRejected: ', fileRejections, event)
alert('You can only upload PDF files to your Omnivore Library.')
setInDragOperation(false)
if ((event as any).preventDefault) {
(event as any).preventDefault();
}
}}
preventDropOnDocument={true}
noClick={true}
accept={{
'text/csv': ['.csv'],
'application/zip': ['.zip'],
'application/pdf': ['.pdf'],
'application/epub+zip': ['.epub'],
}}
>
{({
getRootProps,
getInputProps,
acceptedFiles,
fileRejections,
}) => (
<div
{...getRootProps({ className: 'dropzone' })}
style={{ height: '100%', width: '100%' }}
>
<DragnDropContainer>
<DragnDropStyle>
<DragnDropIndicator
css={{
border: inDragOperation ? '2px dashed blue' : 'unset',
}}
>
<VStack alignment="center" css={{ gap: '10px' }}>
<File
size={48}
color={theme.colors.thTextSubtle2.toString()}
/>
{inDragOperation ? (
<>
<Box
css={{
fontWeight: '800',
fontSize: '20px',
}}
>
Drop to upload your file
</Box>
</>
) : (
<>
<Box
css={{
fontWeight: '800',
fontSize: '20px',
}}
>
Drag files here to add them to your library
</Box>
<Box
css={{
fontSize: '14px',
}}
>
Or{' '}
<a href="" onClick={openDialog}>
choose your files
</a>
</Box>
</>
)}
</VStack>
</DragnDropIndicator>
</DragnDropStyle>
<VStack css={{ width: '100%', mt: '25px', gap: '5px' }}>
{uploadFiles.map((file) => {
return (
<HStack
key={file.id}
css={{
width: '100%',
height: '54px',
border: '1px solid $grayBorder',
borderRadius: '5px',
padding: '15px',
gap: '10px',
color: '$thTextContrast',
}}
alignment="center"
distribution="start"
>
<Box
css={{
width: '280px',
maxLines: '1',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
overflow: 'hidden',
fontSize: '14px',
fontWeight: 'bold',
}}
>
{file.name}
</Box>
{file.status != 'inprogress' ? (
<HStack
alignment="center"
css={{ marginLeft: 'auto', fontSize: '14px' }}
>
{file.status == 'success' && file.openUrl && (
<a href={file.openUrl}>Read Now</a>
)}
{file.status == 'success' && !file.openUrl && (
<span>
{file.message || 'Your import has started'}
</span>
)}
{file.status == 'error' && (
<SpanBox css={{ color: 'red' }}>
Error Uploading
</SpanBox>
)}
</HStack>
) : (
<ProgressRoot value={file.progress} max={100}>
<ProgressIndicator
style={{
transform: `translateX(-${
100 - file.progress
}%)`,
}}
/>{' '}
</ProgressRoot>
)}
</HStack>
)
})}
</VStack>
</DragnDropContainer>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</VStack>
</ModalContent>
</ModalRoot>
)
}