Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage-responses/2): render individual storage mode form response #4001

Merged
merged 11 commits into from
Jul 6, 2022
Merged
3 changes: 2 additions & 1 deletion frontend/src/app/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CreatePage } from '~features/admin-form/create/CreatePage'
import {
FeedbackPage,
FormResultsLayout,
IndividualResponsePage,
ResponsesLayout,
ResponsesPage,
} from '~features/admin-form/responses'
Expand Down Expand Up @@ -90,7 +91,7 @@ export const AppRouter = (): JSX.Element => {
<Route index element={<ResponsesPage />} />
<Route
path=":submissionId"
element={<div>individual response page</div>}
element={<IndividualResponsePage />}
/>
</Route>
<Route
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {
FormSubmissionMetadataQueryDto,
StorageModeSubmissionDto,
StorageModeSubmissionMetadataList,
SubmissionCountQueryDto,
} from '~shared/types/submission'

import formsgSdk from '~utils/formSdk'
import { ApiService } from '~services/ApiService'

import { ADMIN_FORM_ENDPOINT } from '../common/AdminViewFormService'

import { augmentDecryptedResponses } from './ResponsesPage/storage/utils/augmentDecryptedResponses'
import { processDecryptedContent } from './ResponsesPage/storage/utils/processDecryptedContent'

/**
* Counts the number of submissions for a given form
* @param urlParameters Mapping of the url parameters to values
Expand Down Expand Up @@ -47,3 +52,52 @@ export const getFormSubmissionsMetadata = async (
},
).then(({ data }) => data)
}

/**
* Returns the data of a single submission of a given storage mode form
* @param arg.formId The id of the form to query
* @param arg.submissionId The id of the submission
* @returns The data of the submission
*/
const getEncryptedSubmissionById = async ({
formId,
submissionId,
}: {
formId: string
submissionId: string
}): Promise<StorageModeSubmissionDto> => {
return ApiService.get<StorageModeSubmissionDto>(
`${ADMIN_FORM_ENDPOINT}/${formId}/submissions/${submissionId}`,
).then(({ data }) => data)
}

export const getDecryptedSubmissionById = async ({
formId,
submissionId,
secretKey,
}: {
formId: string
submissionId: string
secretKey?: string
}) => {
if (!secretKey) return

const { content, version, verified, attachmentMetadata, ...rest } =
await getEncryptedSubmissionById({ formId, submissionId })

const decryptedContent = formsgSdk.crypto.decrypt(secretKey, {
encryptedContent: content,
verifiedContent: verified,
version,
})
if (!decryptedContent) throw new Error('Could not decrypt the response')
const processedContent = augmentDecryptedResponses(
processDecryptedContent(decryptedContent),
attachmentMetadata,
)
// Add metadata for display.
return {
...rest,
responses: processedContent,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { memo, useCallback } from 'react'
import { BiDownload } from 'react-icons/bi'
import { useMutation } from 'react-query'
import { Stack, Table, Tbody, Td, Text, Tr } from '@chakra-ui/react'
import FileSaver from 'file-saver'

import { BasicField } from '~shared/types'

import Button from '~components/Button'
import FormLabel from '~components/FormControl/FormLabel'

import { AugmentedDecryptedResponse } from '../ResponsesPage/storage/utils/augmentDecryptedResponses'
import { downloadAndDecryptAttachment } from '../ResponsesPage/storage/utils/downloadAndDecryptAttachment'

export interface DecryptedRowBaseProps {
row: AugmentedDecryptedResponse
}
type DecryptedRowProps = DecryptedRowBaseProps & {
secretKey: string
}

const DecryptedQuestionLabel = ({ row }: DecryptedRowBaseProps) => {
return (
<FormLabel questionNumber={`${row.questionNumber}.`} isRequired>
{row.question}
</FormLabel>
)
}

const DecryptedHeaderRow = ({ row }: DecryptedRowBaseProps): JSX.Element => {
return (
<Text
textStyle="h2"
as="h2"
color="primary.500"
mb="0.5rem"
_notFirst={{ mt: '2.5rem' }}
>
{row.signature ? `[verified] ` : ''}
{row.question}
</Text>
)
}

const DecryptedTableRow = ({ row }: DecryptedRowBaseProps): JSX.Element => {
return (
<Stack>
<DecryptedQuestionLabel row={row} />
<Table variant="column-stripe" sx={{ tableLayout: 'fixed' }}>
<Tbody>
{row.answerArray?.map((row, idx) => (
<Tr key={idx}>
{Array.isArray(row) ? (
row.map((col, cidx) => <Td key={cidx}>{col}</Td>)
) : (
<Td>{row}</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</Stack>
)
}

const DecryptedAttachmentRow = ({ row, secretKey }: DecryptedRowProps) => {
const handleDownloadMutation = useMutation(async (url: string) => {
const byteArray = await downloadAndDecryptAttachment(url, secretKey)
if (!byteArray) throw new Error('Invalid file')
return FileSaver.saveAs(new Blob([byteArray]), row.answer)
})

const handleDownload = useCallback(() => {
if (!row.downloadUrl) return
return handleDownloadMutation.mutate(row.downloadUrl)
}, [handleDownloadMutation, row.downloadUrl])

return (
<Stack>
<DecryptedQuestionLabel row={row} />
<Text textStyle="body-1">
Filename:{' '}
{row.answer && (
<Button
variant="link"
aria-label="Download file"
isDisabled={handleDownloadMutation.isLoading}
onClick={handleDownload}
rightIcon={<BiDownload fontSize="1.5rem" />}
>
{row.answer}
</Button>
)}
</Text>
</Stack>
)
}

export const DecryptedRow = memo(
({ row, secretKey }: DecryptedRowProps): JSX.Element => {
switch (row.fieldType) {
case BasicField.Section:
return <DecryptedHeaderRow row={row} />
case BasicField.Attachment:
return <DecryptedAttachmentRow row={row} secretKey={secretKey} />
case BasicField.Table:
return <DecryptedTableRow row={row} />
default:
return (
<Stack>
<DecryptedQuestionLabel row={row} />
{row.answer && <Text textStyle="body-1">{row.answer}</Text>}
{row.answerArray && (
<Text textStyle="body-1">{row.answerArray.join(', ')}</Text>
)}
</Stack>
)
}
},
)
Loading