Skip to content

Commit

Permalink
feat: add download button to download responses
Browse files Browse the repository at this point in the history
  • Loading branch information
karrui committed Jun 9, 2022
1 parent 530fc85 commit 69d93b9
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export const ResponsesTabWrapper = ({
return (
<Container
overflowY="auto"
p={{ base: '1.5rem', md: '3rem' }}
px={{ base: '1.5rem', md: '1.25rem' }}
py={{ base: '1.5rem', md: '3rem' }}
maxW="69.5rem"
flex={1}
display="flex"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createContext, useContext } from 'react'

import { DownloadEncryptedParams } from './useDecryptionWorkers'

export interface StorageResponsesContextProps {
secretKey?: string
setSecretKey: (secretKey: string) => void
handleExportCsv: () => void
downloadParams: Omit<DownloadEncryptedParams, 'downloadAttachments'> | null
responsesCount?: number
formPublicKey: string | null
isLoading: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
import { useCallback, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useParams } from 'react-router-dom'

import { FormResponseMode } from '~shared/types'

import { useAdminForm } from '~features/admin-form/common/queries'

import { useFormResponsesCount } from '../../queries'
import useDecryptionWorkers from '../../useDecryptionWorkers'

import { StorageResponsesContext } from './StorageResponsesContext'
import { useSecretKey } from './useSecretKey'

export const StorageResponsesProvider = ({
children,
}: {
children: React.ReactNode
}): JSX.Element => {
const { formId } = useParams()
const { downloadEncryptedResponses } = useDecryptionWorkers()
if (!formId) throw new Error('No formId provided')

const { data: form, isLoading: isAdminFormLoading } = useAdminForm()
const { data: responsesCount, isLoading: isFormResponsesLoading } =
useFormResponsesCount()
const [secretKey, setSecretKey] = useState<string>()

const handleExportCsv = useCallback(() => {
if (!formId || !form?.title || !secretKey) return
return downloadEncryptedResponses(formId, form.title, secretKey)
}, [downloadEncryptedResponses, formId, secretKey, form?.title])
const [secretKey, setSecretKey] = useSecretKey(formId)

const formPublicKey = useMemo(() => {
if (!form || form.responseMode !== FormResponseMode.Encrypt) return null
return form.publicKey
}, [form])

const downloadParams = useMemo(() => {
if (!secretKey) return null

return {
secretKey,
// TODO: Add selector for start and end dates.
endDate: undefined,
startDate: undefined,
}
}, [secretKey])

return (
<StorageResponsesContext.Provider
value={{
isLoading: isAdminFormLoading || isFormResponsesLoading,
formPublicKey,
responsesCount,
handleExportCsv,
downloadParams,
secretKey,
setSecretKey,
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { StorageModeSubmissionMetadata } from '~shared/types/submission'

import Button from '~components/Button'

import { useFormResponses } from '../../queries'
import { EmptyResponses } from '../common/EmptyResponses'
import { ResponsesTabWrapper } from '../common/ResponsesTabWrapper'

import { SecretKeyVerification } from './SecretKeyVerification'
import { useStorageResponsesContext } from './StorageResponsesContext'
import { StorageResponsesProvider } from './StorageResponsesProvider'
import { UnlockedResponses } from './UnlockedResponses'

export const StorageResponsesTab = () => {
return (
Expand All @@ -19,31 +15,15 @@ export const StorageResponsesTab = () => {
}

const ProvidedStorageResponsesTab = (): JSX.Element => {
const { responsesCount, secretKey, handleExportCsv } =
useStorageResponsesContext()
const { data } = useFormResponses()
const { responsesCount, secretKey } = useStorageResponsesContext()

if (responsesCount === 0) {
return <EmptyResponses />
}

return (
<ResponsesTabWrapper>
{secretKey ? (
<>
<Button onClick={handleExportCsv}>Export csv</Button>
<Button>Export csv and attachments</Button>
{data?.metadata.map((submission: StorageModeSubmissionMetadata) => {
return (
<div key={submission.refNo}>
Submission Ref No: {submission.refNo}
</div>
)
})}
</>
) : (
<SecretKeyVerification />
)}
{secretKey ? <UnlockedResponses /> : <SecretKeyVerification />}
</ResponsesTabWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useCallback } from 'react'
import { useMutation } from 'react-query'
import { Box, MenuButton } from '@chakra-ui/react'

import { BxsChevronDown } from '~assets/icons/BxsChevronDown'
import { BxsChevronUp } from '~assets/icons/BxsChevronUp'
import Badge from '~components/Badge'
import Button from '~components/Button'
import Menu from '~components/Menu'

import { useStorageResponsesContext } from '../StorageResponsesContext'
import useDecryptionWorkers, {
DownloadEncryptedParams,
} from '../useDecryptionWorkers'

export const DownloadButton = (): JSX.Element => {
const { downloadEncryptedResponses } = useDecryptionWorkers()
const { downloadParams } = useStorageResponsesContext()

const handleExportCsvMutation = useMutation(
(params: DownloadEncryptedParams) => downloadEncryptedResponses(params),
// TODO: add error and success handling
)

const handleExportCsvNoAttachments = useCallback(() => {
if (!downloadParams) return
return handleExportCsvMutation.mutate({
...downloadParams,
downloadAttachments: false,
})
}, [downloadParams, handleExportCsvMutation])

const handleExportCsvWithAttachments = useCallback(() => {
if (!downloadParams) return
return handleExportCsvMutation.mutate({
...downloadParams,
downloadAttachments: true,
})
}, [downloadParams, handleExportCsvMutation])

return (
<Box gridArea="export" justifySelf="flex-end">
<Menu placement="bottom-end">
{({ isOpen }) => (
<>
<MenuButton
as={Button}
isDisabled={!downloadParams}
isLoading={handleExportCsvMutation.isLoading}
isActive={isOpen}
aria-label="Download options"
rightIcon={isOpen ? <BxsChevronUp /> : <BxsChevronDown />}
>
Download
</MenuButton>
<Menu.List>
<Menu.Item onClick={handleExportCsvNoAttachments}>
CSV only
</Menu.Item>
<Menu.Item onClick={handleExportCsvWithAttachments}>
CSV with attachments
<Badge ml="0.5rem" colorScheme="success">
beta
</Badge>
</Menu.Item>
</Menu.List>
</>
)}
</Menu>
</Box>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMemo } from 'react'
import { Box, Flex, Grid, Text } from '@chakra-ui/react'
import simplur from 'simplur'

import { useFormResponses } from '../../../queries'

import { DownloadButton } from './DownloadButton'

export const UnlockedResponses = (): JSX.Element => {
const { data: { count } = {} } = useFormResponses()

const prettifiedResponsesCount = useMemo(() => {
if (!count) return
return simplur` ${[count]}response[|s] to date`
}, [count])

return (
<Flex flexDir="column" h="100%">
<Grid
mb="1rem"
alignItems="end"
color="secondary.500"
gridTemplateColumns={{ base: 'auto', md: 'auto 1fr' }}
gridGap={{ base: '0.5rem', md: '1.5rem' }}
gridTemplateAreas={{
base: "'submissions submissions' 'export'",
md: "'submissions export'",
}}
>
<Box gridArea="submissions">
<Text textStyle="h4">
<Text as="span" color="primary.500">
{count?.toLocaleString()}
</Text>
{prettifiedResponsesCount}
</Text>
</Box>
<DownloadButton />
</Grid>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UnlockedResponses } from './UnlockedResponses'
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,21 @@ export type DownloadEncryptedParams = EncryptedResponsesStreamParams & {
const useDecryptionWorkers = () => {
const [workers, setWorkers] = useState<CleanableDecryptionWorkerApi[]>([])
const abortControllerRef = useRef(new AbortController())

const { data: adminForm } = useAdminForm()

const { refetch } = useFormResponsesCount()

useEffect(() => {
const abortController = abortControllerRef.current
return () => killWorkers(workers)
}, [workers])

useEffect(() => {
const abortController = abortControllerRef.current
return () => {
killWorkers(workers)
abortController.abort()
}
}, [workers])
}, [])

const downloadEncryptedResponses = useCallback(
async ({
Expand All @@ -53,8 +56,6 @@ const useDecryptionWorkers = () => {
throwOnError: true,
})
if (!responsesCount) return
// Abort any existing downloads if this function is re-invoked.
abortControllerRef.current.abort()
if (workers.length) killWorkers(workers)

// Create a pool of decryption workers
Expand Down

0 comments on commit 69d93b9

Please sign in to comment.