From c66e9b688cfc4b402502e6413820d51bc3542e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Mon, 17 Oct 2022 11:09:55 +0000 Subject: [PATCH] feat(j-s): Create police case files screen (#8600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create police case files screen * Basic page setup * feat(j-s): rewrite useS3Upload hook * feat(j-s): use new upload hook for each loke number * feat(j-s): add retry and remove for useS3UploadV2 * feat(j-s): add error handling for upload Co-authored-by: Ívar Oddsson * feat(j-s): memorize police file upload list To avoid maximum depth rendering * feat(j-s): move string to contentful * docs(j-s): add to more explaination to comment * refactor(j-s): change PageTitle to accept children * fix(j-s): fix missing defaultMessage for contentful string * fix(j-s): fix retry uploading file Adding `id` from the beginning to file to be able to refrence it when retrying. Setting precent and state to uploading for better user experince when retrying a failed upload. * feat(j-s): handle errors when delete file failes * test(j-s): fix e2e test for indictments prosecutor flow * refactor(j-s): move setting file upload state out of uploadToS3 Co-authored-by: Arnar Kári Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Prosecutor/Indictments/caseFiles.spec.ts | 4 +- .../Indictments/policeCaseFiles.spec.ts | 28 ++ .../web/messages/Core/errors.ts | 5 + .../web/messages/Core/sections.ts | 7 + .../web/messages/Core/titles.ts | 7 + .../web/pages/krafa/akaera/akaerdi/[id].ts | 2 +- .../web/pages/krafa/akaera/malsgogn/[id].ts | 3 + .../web/pages/krafa/ny/akaera.ts | 2 +- .../src/components/FormProvider/caseGql.ts | 1 + .../src/components/PageTitle/PageTitle.tsx | 7 +- .../Indictments/CourtRecord/CourtRecord.tsx | 2 +- .../ProsecutorAndDefender.tsx | 2 +- .../Court/Indictments/Subpoena/Subpoena.tsx | 2 +- .../Indictments/CaseFiles/CaseFiles.tsx | 50 +--- .../Indictments/{ => Defendant}/Defendant.tsx | 2 +- .../Indictments/Overview/Overview.tsx | 2 +- .../PoliceCaseFilesRoute.strings.ts | 38 +++ .../PoliceCaseFiles/PoliceCaseFilesRoute.tsx | 279 ++++++++++++++++++ .../PoliceCaseFiles.strings.ts | 5 +- .../PoliceCaseFiles/PoliceCaseFiles.tsx | 4 +- .../src/routes/Shared/Overview/Overview.tsx | 2 +- apps/judicial-system/web/src/types/index.ts | 3 +- .../web/src/utils/hooks/index.ts | 1 + .../web/src/utils/hooks/useS3Upload/index.ts | 4 + .../hooks/useS3UploadV2/useS3UploadV2.ts | 172 +++++++++++ .../web/src/utils/hooks/useSections/index.ts | 13 + libs/judicial-system/consts/src/lib/consts.ts | 1 + 27 files changed, 581 insertions(+), 67 deletions(-) create mode 100644 apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/policeCaseFiles.spec.ts create mode 100644 apps/judicial-system/web/pages/krafa/akaera/malsgogn/[id].ts rename apps/judicial-system/web/src/routes/Prosecutor/Indictments/{ => Defendant}/Defendant.tsx (99%) create mode 100644 apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.strings.ts create mode 100644 apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.tsx create mode 100644 apps/judicial-system/web/src/utils/hooks/useS3UploadV2/useS3UploadV2.ts diff --git a/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/caseFiles.spec.ts b/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/caseFiles.spec.ts index 114da7ebb660..a209fcf12211 100644 --- a/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/caseFiles.spec.ts +++ b/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/caseFiles.spec.ts @@ -1,6 +1,6 @@ import { INDICTMENTS_CASE_FILES_ROUTE, - INDICTMENTS_OVERVIEW_ROUTE, + INDICTMENTS_POLICE_CASE_FILES_ROUTE, } from '@island.is/judicial-system/consts' import { CaseType, UserRole } from '@island.is/judicial-system/types' @@ -23,6 +23,6 @@ describe(`${INDICTMENTS_CASE_FILES_ROUTE}/:id`, () => { it('should navigate to the correct page on continue', () => { cy.getByTestid('continueButton').click() - cy.url().should('contain', INDICTMENTS_OVERVIEW_ROUTE) + cy.url().should('contain', INDICTMENTS_POLICE_CASE_FILES_ROUTE) }) }) diff --git a/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/policeCaseFiles.spec.ts b/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/policeCaseFiles.spec.ts new file mode 100644 index 000000000000..f25a144da1b1 --- /dev/null +++ b/apps/judicial-system/web-e2e/src/integration/Prosecutor/Indictments/policeCaseFiles.spec.ts @@ -0,0 +1,28 @@ +import { + INDICTMENTS_POLICE_CASE_FILES_ROUTE, + INDICTMENTS_OVERVIEW_ROUTE, +} from '@island.is/judicial-system/consts' +import { CaseType, UserRole } from '@island.is/judicial-system/types' + +import { makeCourt, mockCase, makeProsecutor, intercept } from '../../../utils' + +describe(`${INDICTMENTS_POLICE_CASE_FILES_ROUTE}/:id`, () => { + beforeEach(() => { + const caseData = mockCase(CaseType.MURDER) + const caseDataAddition = { + ...caseData, + prosecutor: makeProsecutor(), + court: makeCourt(), + } + + cy.login(UserRole.PROSECUTOR) + cy.stubAPIResponses() + intercept(caseDataAddition) + cy.visit(`${INDICTMENTS_POLICE_CASE_FILES_ROUTE}/test_id`) + }) + + it('should navigate to the correct page on continue', () => { + cy.getByTestid('continueButton').click() + cy.url().should('contain', INDICTMENTS_OVERVIEW_ROUTE) + }) +}) diff --git a/apps/judicial-system/web/messages/Core/errors.ts b/apps/judicial-system/web/messages/Core/errors.ts index fbfd96994317..b39f2cb79478 100644 --- a/apps/judicial-system/web/messages/Core/errors.ts +++ b/apps/judicial-system/web/messages/Core/errors.ts @@ -81,4 +81,9 @@ export const errors = defineMessages({ defaultMessage: 'Ekki tókst að afrita hlekk', description: 'Notaður sem villuskilaboð þegar ekki gengur að afrita hlekk', }, + failedDeleteFile: { + id: 'judicial.system.core:errors.failed_delete_file', + defaultMessage: 'Ekki tókst að eyða skrá', + description: 'Notaður sem villuskilaboð þegar ekki gengur að eyða skrá', + }, }) diff --git a/apps/judicial-system/web/messages/Core/sections.ts b/apps/judicial-system/web/messages/Core/sections.ts index 2f9dc4aaa657..a9d202dfd88c 100644 --- a/apps/judicial-system/web/messages/Core/sections.ts +++ b/apps/judicial-system/web/messages/Core/sections.ts @@ -122,6 +122,13 @@ export const sections = { description: 'Notaður sem texti fyrir Dómskjöl skref í hliðarstiku í ákærum hjá sækjendum', }, + policeCaseFiles: { + id: + 'judicial.system.core:sections.indictment_case_prosecutor_section.police_case_files', + defaultMessage: 'Málsgögn', + description: + 'Notaður sem texti fyrir Málsgögn skref í hliðarstiku í ákærum hjá sækjendum', + }, overview: { id: 'judicial.system.core:sections.indictment_case_prosecutor_section.overview', diff --git a/apps/judicial-system/web/messages/Core/titles.ts b/apps/judicial-system/web/messages/Core/titles.ts index 8486cb13672e..8956740f8849 100644 --- a/apps/judicial-system/web/messages/Core/titles.ts +++ b/apps/judicial-system/web/messages/Core/titles.ts @@ -108,6 +108,13 @@ export const titles = { description: 'Notaður sem titill fyrir Dómskjöl skjá hjá saksóknara í ákærum', }), + policeCaseFiles: defineMessage({ + id: + 'judicial.system.core:titles.prosecutor.indictments.police_case_files', + defaultMessage: 'Málsgögn - Réttarvörslugátt', + description: + 'Notaður sem titill fyrir Málsgögn skjá hjá saksóknara í ákærum', + }), overview: defineMessage({ id: 'judicial.system.core:titles.prosecutor.indictments.overview', defaultMessage: 'Yfirlit ákæru - Réttarvörslugátt', diff --git a/apps/judicial-system/web/pages/krafa/akaera/akaerdi/[id].ts b/apps/judicial-system/web/pages/krafa/akaera/akaerdi/[id].ts index d6c5ed454e26..8e7a6393209b 100644 --- a/apps/judicial-system/web/pages/krafa/akaera/akaerdi/[id].ts +++ b/apps/judicial-system/web/pages/krafa/akaera/akaerdi/[id].ts @@ -1,3 +1,3 @@ -import Defendant from '@island.is/judicial-system-web/src/routes/Prosecutor/Indictments/Defendant' +import Defendant from '@island.is/judicial-system-web/src/routes/Prosecutor/Indictments/Defendant/Defendant' export default Defendant diff --git a/apps/judicial-system/web/pages/krafa/akaera/malsgogn/[id].ts b/apps/judicial-system/web/pages/krafa/akaera/malsgogn/[id].ts new file mode 100644 index 000000000000..55aa88a412f6 --- /dev/null +++ b/apps/judicial-system/web/pages/krafa/akaera/malsgogn/[id].ts @@ -0,0 +1,3 @@ +import PoliceCaseFiles from '@island.is/judicial-system-web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute' + +export default PoliceCaseFiles diff --git a/apps/judicial-system/web/pages/krafa/ny/akaera.ts b/apps/judicial-system/web/pages/krafa/ny/akaera.ts index 0aa7451b36ad..a3efd74029e6 100644 --- a/apps/judicial-system/web/pages/krafa/ny/akaera.ts +++ b/apps/judicial-system/web/pages/krafa/ny/akaera.ts @@ -1,2 +1,2 @@ -import Defendant from '@island.is/judicial-system-web/src/routes/Prosecutor/Indictments/Defendant' +import Defendant from '@island.is/judicial-system-web/src/routes/Prosecutor/Indictments/Defendant/Defendant' export default Defendant diff --git a/apps/judicial-system/web/src/components/FormProvider/caseGql.ts b/apps/judicial-system/web/src/components/FormProvider/caseGql.ts index a0ac2a037501..dd4d1066a40b 100644 --- a/apps/judicial-system/web/src/components/FormProvider/caseGql.ts +++ b/apps/judicial-system/web/src/components/FormProvider/caseGql.ts @@ -146,6 +146,7 @@ const CaseQuery = gql` state key category + policeCaseNumber } isAppealDeadlineExpired isAppealGracePeriodExpired diff --git a/apps/judicial-system/web/src/components/PageTitle/PageTitle.tsx b/apps/judicial-system/web/src/components/PageTitle/PageTitle.tsx index 2643e0691ee6..189789466812 100644 --- a/apps/judicial-system/web/src/components/PageTitle/PageTitle.tsx +++ b/apps/judicial-system/web/src/components/PageTitle/PageTitle.tsx @@ -2,13 +2,10 @@ import React from 'react' import { Box, Text } from '@island.is/island-ui/core' -interface Props { - title: string -} -const PageTitle: React.FC = ({ title }) => ( +const PageTitle: React.FC = ({ children }) => ( - {title} + {children} ) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/CourtRecord/CourtRecord.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/CourtRecord/CourtRecord.tsx index 9c2d72ab233e..5566eebb568e 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/CourtRecord/CourtRecord.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/CourtRecord/CourtRecord.tsx @@ -76,7 +76,7 @@ const CourtRecord: React.FC = () => { > - + {formatMessage(m.title)} { title={formatMessage(titles.court.indictments.prosecutorAndDefender)} /> - + {formatMessage(m.title)} diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx index f8d3584c12a7..3920ecb1fedb 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx @@ -100,7 +100,7 @@ const Subpoena: React.FC = () => { > - + {formatMessage(strings.title)} { onRetry={handleRetry} /> - + {formatMessage(strings.caseFiles.sections.costBreakdown)} @@ -145,57 +145,11 @@ const CaseFiles: React.FC = () => { onRetry={handleRetry} /> - - - - {formatMessage(strings.caseFiles.sections.caseFileContents)} - - - - {` *`} - - - file.category === CaseFileCategory.CASE_FILE_CONTENTS, - )} - header={formatMessage(strings.caseFiles.sections.inputFieldLabel)} - buttonLabel={formatMessage(strings.caseFiles.sections.buttonLabel)} - onChange={(files) => - handleS3Upload(files, false, CaseFileCategory.CASE_FILE_CONTENTS) - } - onRemove={handleRemoveFromS3} - onRetry={handleRetry} - /> - - - - - {formatMessage(strings.caseFiles.sections.caseFile)} - - - - {` *`} - - - file.category === CaseFileCategory.CASE_FILE, - )} - header={formatMessage(strings.caseFiles.sections.inputFieldLabel)} - buttonLabel={formatMessage(strings.caseFiles.sections.buttonLabel)} - onChange={(files) => - handleS3Upload(files, false, CaseFileCategory.CASE_FILE) - } - onRemove={handleRemoveFromS3} - onRetry={handleRetry} - /> - diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx similarity index 99% rename from apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant.tsx rename to apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx index 8baf35b037a5..c64e78ec85c5 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx @@ -40,7 +40,7 @@ import { DefendantInfo, PoliceCaseNumbers, usePoliceCaseNumbers, -} from '../components' +} from '../../components' const Defendant: React.FC = () => { const { diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx index f82b6d10dd0f..e3ca5bb308a9 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx @@ -113,7 +113,7 @@ const Overview: React.FC = () => { previousUrl={ caseHasBeenSentToCourt ? constants.CASES_ROUTE - : `${constants.INDICTMENTS_CASE_FILES_ROUTE}/${workingCase.id}` + : `${constants.INDICTMENTS_POLICE_CASE_FILES_ROUTE}/${workingCase.id}` } nextButtonText={formatMessage(strings.overview.nextButtonText, { isNewIndictment, diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.strings.ts new file mode 100644 index 000000000000..7d8e9f50b2a3 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.strings.ts @@ -0,0 +1,38 @@ +import { defineMessage, defineMessages } from 'react-intl' + +export const policeCaseFiles = { + heading: defineMessage({ + id: 'judicial.system.core:police_case_files_route.heading', + defaultMessage: 'Málsgögn', + description: 'Notaður sem titill á Málsgögn skrefi í ákærum.', + }), + infoBox: defineMessage({ + id: 'judicial.system.core:police_case_files_route.info_box', + defaultMessage: + 'Gögn sem er hlaðið upp hér fyrir neðan verða sameinuð í eitt PDF skjal og efnisyfirlit sjálfkrafa búið til.', + description: 'Notaður sem texti í info boxi í Málsgögn skrefi í ákærum.', + }), + policeCaseNumberSectionHeading: defineMessage({ + id: + 'judicial.system.core:police_case_files_route.police_case_number_section_heading', + defaultMessage: 'Gögn úr LÖKE-máli {policeCaseNumber}', + description: + 'Notaður sem fyrirsögn fyrir hvert Löke númer á Málsgögn skrefi í ákærum.', + }), + inputFileUpload: defineMessages({ + header: { + id: + 'judicial.system.core:police_case_files_route.input_file_upload.header', + defaultMessage: 'Dragðu skrár hingað til að hlaða upp', + description: + 'Notaður fyrir texta í svæði sem hægt er að draga skrá á til að hlaða þeim upp.', + }, + buttonLabel: { + id: + 'judicial.system.core:police_case_files_route.input_file_upload.button_label', + defaultMessage: 'Velja gögn til að hlaða upp', + description: + 'Notaður fyrir texta í takka sem hægt er að ýta á til að velja skrár til að hlaða upp.', + }, + }), +} diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.tsx new file mode 100644 index 000000000000..4ef16cd4a4ba --- /dev/null +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/PoliceCaseFiles/PoliceCaseFilesRoute.tsx @@ -0,0 +1,279 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import { useIntl } from 'react-intl' +import { uuid } from 'uuidv4' + +import { + FormContentContainer, + FormContext, + FormFooter, + InfoBox, + PageHeader, + PageLayout, + PageTitle, + ProsecutorCaseInfo, + SectionHeading, +} from '@island.is/judicial-system-web/src/components' +import { + IndictmentsProsecutorSubsections, + Sections, +} from '@island.is/judicial-system-web/src/types' +import { + titles, + errors as errorMessages, +} from '@island.is/judicial-system-web/messages' +import { + Box, + InputFileUpload, + toast, + UploadFile, +} from '@island.is/island-ui/core' +import { CaseFile, CaseFileCategory } from '@island.is/judicial-system/types' +import { useS3UploadV2 } from '@island.is/judicial-system-web/src/utils/hooks' +import * as constants from '@island.is/judicial-system/consts' + +import { policeCaseFiles as m } from './PoliceCaseFilesRoute.strings' + +const mapCaseFileToUploadFile = (file: CaseFile): UploadFile => ({ + name: file.name, + type: file.type, + id: file.id, + key: file.key, + status: 'done', + percent: 100, + size: file.size, +}) + +const UploadFilesToPoliceCase: React.FC<{ + caseId: string + policeCaseNumber: string + setAllUploaded: (allUploaded: boolean) => void + caseFiles: CaseFile[] +}> = ({ caseId, policeCaseNumber, setAllUploaded, caseFiles }) => { + const { formatMessage } = useIntl() + const { upload, remove } = useS3UploadV2( + caseId, + CaseFileCategory.CASE_FILE, + policeCaseNumber, + ) + + const [displayFiles, setDisplayFiles] = useState( + caseFiles.map(mapCaseFileToUploadFile), + ) + + const errorMessage = useMemo(() => { + if (displayFiles.some((file) => file.status === 'error')) { + return formatMessage(errorMessages.general) + } else { + return undefined + } + }, [displayFiles, formatMessage]) + + useEffect(() => { + setDisplayFiles(caseFiles.map(mapCaseFileToUploadFile)) + }, [caseFiles, setDisplayFiles]) + + useEffect(() => { + const isUploading = displayFiles.some((file) => file.status === 'uploading') + setAllUploaded(isUploading) + }, [setAllUploaded, displayFiles]) + + const setSingleFile = useCallback( + (displayFile) => { + setDisplayFiles((previous) => { + const index = previous.findIndex((f) => f.id === displayFile.id) + if (index === -1) { + return previous + } + const next = [...previous] + next[index] = displayFile + return next + }) + }, + [setDisplayFiles], + ) + + const onChange = useCallback( + (files: File[]) => { + // We generate an id for each file so that we find the file again when + // updating the file's progress and onRetry. + // Also we cannot spread File since it contains read-only properties. + const filesWithId: Array<[File, string]> = files.map((file) => [ + file, + `${file.name}-${uuid()}`, + ]) + setDisplayFiles((previous) => [ + ...filesWithId.map( + ([file, id]): UploadFile => ({ + status: 'uploading', + percent: 1, + name: file.name, + id: id, + type: file.type, + }), + ), + ...previous, + ]) + upload(filesWithId, setSingleFile) + }, + [upload, setSingleFile], + ) + + const onRetry = useCallback( + (file: UploadFile) => { + setSingleFile({ + name: file.name, + id: file.id, + percent: 1, + status: 'uploading', + type: file.type, + }) + upload( + [ + [ + { name: file.name, type: file.type ?? '' } as File, + file.id ?? file.name, + ], + ], + setSingleFile, + ) + }, + [upload, setSingleFile], + ) + + const onRemove = useCallback( + async (file: UploadFile) => { + try { + const response = await remove(file.id) + if (!response.data?.deleteFile.success) { + throw new Error(`Failed to delete file: ${file.id}`) + } + + setDisplayFiles((previous) => { + return previous.filter((f) => f.id !== file.id) + }) + } catch (e) { + toast.error(formatMessage(errorMessages.failedDeleteFile)) + } + }, + [remove, setDisplayFiles, formatMessage], + ) + + return ( + + ) +} + +type allUploadedState = { + [policeCaseNumber: string]: boolean +} + +/* We need to make sure this list is not rerenderd unless the props are changing. + * Since we passing `setAllUploaded` to the children and they are calling it within a useEffect + * causing a endless rendering loop. + */ +const PoliceUploadListMemo: React.FC<{ + caseId: string + policeCaseNumbers: string[] + caseFiles?: CaseFile[] + setAllUploaded: (policeCaseNumber: string) => (value: boolean) => void +}> = memo(({ caseId, policeCaseNumbers, caseFiles, setAllUploaded }) => { + const { formatMessage } = useIntl() + return ( + + {policeCaseNumbers.map((policeCaseNumber, index) => ( + + + file.policeCaseNumber === policeCaseNumber, + ) ?? [] + } + policeCaseNumber={policeCaseNumber} + setAllUploaded={setAllUploaded(policeCaseNumber)} + /> + + ))} + + ) +}) + +const PoliceCaseFilesRoute = () => { + const { formatMessage } = useIntl() + const { workingCase, isLoadingWorkingCase, caseNotFound } = useContext( + FormContext, + ) + + const [allUploaded, setAllUploaded] = useState( + workingCase.policeCaseNumbers.reduce( + (acc, policeCaseNumber) => ({ ...acc, [policeCaseNumber]: true }), + {}, + ), + ) + + const setAllUploadedForPoliceCaseNumber = useCallback( + (number: string) => (value: boolean) => { + setAllUploaded((previous) => ({ ...previous, [number]: value })) + }, + [setAllUploaded], + ) + + return ( + + + + {formatMessage(m.heading)} + + + + + + + + v)} + nextIsLoading={isLoadingWorkingCase} + /> + + + ) +} + +export default PoliceCaseFilesRoute diff --git a/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.strings.ts index e93558c29502..17152288b0c3 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.strings.ts +++ b/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.strings.ts @@ -2,8 +2,9 @@ import { defineMessages } from 'react-intl' export const policeCaseFiles = defineMessages({ heading: { - id: 'judicial.system.core:police_case_files.heading', - defaultMessage: 'Gögn úr LÖKE', + id: 'judicial.system.core:police_case_files.heading_v1', + defaultMessage: + 'Gögn úr LÖKE{policeCaseNumber, select, NONE {} other {-máli {policeCaseNumber}}}', description: 'Notaður sem titill fyrir "LOKE" gagnapakkann á rannsóknargagna skrefi.', }, diff --git a/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.tsx b/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.tsx index e547777198c1..5ed70b853703 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/components/PoliceCaseFiles/PoliceCaseFiles.tsx @@ -103,9 +103,11 @@ interface Props { setPoliceCaseFileList: React.Dispatch< React.SetStateAction > + policeCaseNumber?: string } const PoliceCaseFiles: React.FC = ({ + policeCaseNumber, isUploading, setIsUploading, policeCaseFileList, @@ -253,7 +255,7 @@ const PoliceCaseFiles: React.FC = ({ return ( <> diff --git a/apps/judicial-system/web/src/routes/Shared/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Shared/Overview/Overview.tsx index 47cba6ad1bdc..17b88a2545d8 100644 --- a/apps/judicial-system/web/src/routes/Shared/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Overview/Overview.tsx @@ -57,7 +57,7 @@ const Overview = () => { > - + {formatMessage(m.title)} {caseIsClosed ? ( diff --git a/apps/judicial-system/web/src/types/index.ts b/apps/judicial-system/web/src/types/index.ts index 51b34b811fda..4e3bc79ffc87 100644 --- a/apps/judicial-system/web/src/types/index.ts +++ b/apps/judicial-system/web/src/types/index.ts @@ -35,7 +35,8 @@ export enum IndictmentsProsecutorSubsections { DEFENDANT = 0, PROCESSING = 1, CASE_FILES = 2, - OVERVIEW = 3, + POLICE_CASE_FILES = 3, + OVERVIEW = 4, } export enum IndictmentsCourtSubsections { diff --git a/apps/judicial-system/web/src/utils/hooks/index.ts b/apps/judicial-system/web/src/utils/hooks/index.ts index efc5bc7568ff..c82a6a02d88b 100644 --- a/apps/judicial-system/web/src/utils/hooks/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/index.ts @@ -4,6 +4,7 @@ export { default as useCase } from './useCase' export { default as useFileList } from './useFileList' export { default as useInstitution } from './useInstitution' export { useS3Upload } from './useS3Upload' +export { useS3UploadV2 } from './useS3UploadV2/useS3UploadV2' export { useGetLawyers, useGetLawyer } from './useLawyers/useLawyers' export { default as useDeb } from './useDeb' export { default as useViewport } from './useViewport/useViewport' diff --git a/apps/judicial-system/web/src/utils/hooks/useS3Upload/index.ts b/apps/judicial-system/web/src/utils/hooks/useS3Upload/index.ts index 0ec3f8aa9cbb..bd3304329050 100644 --- a/apps/judicial-system/web/src/utils/hooks/useS3Upload/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useS3Upload/index.ts @@ -19,6 +19,7 @@ import { errors } from '@island.is/judicial-system-web/messages' export interface TUploadFile extends UploadFile { category?: CaseFileCategory + policeCaseNumber?: string } export const useS3Upload = (workingCase: Case) => { @@ -215,6 +216,7 @@ export const useS3Upload = (workingCase: Case) => { key: file.key, size: file.size, category: file.category, + policeCaseNumber: file.policeCaseNumber, }, }, }) @@ -234,9 +236,11 @@ export const useS3Upload = (workingCase: Case) => { newFiles: File[], isRetry?: boolean, filesCategory?: CaseFileCategory, + policeCaseNumber?: string, ) => { newFiles.forEach(async (file: TUploadFile) => { file.category = filesCategory + file.policeCaseNumber = policeCaseNumber }) if (!isRetry) { diff --git a/apps/judicial-system/web/src/utils/hooks/useS3UploadV2/useS3UploadV2.ts b/apps/judicial-system/web/src/utils/hooks/useS3UploadV2/useS3UploadV2.ts new file mode 100644 index 000000000000..d6d9ea8d1d87 --- /dev/null +++ b/apps/judicial-system/web/src/utils/hooks/useS3UploadV2/useS3UploadV2.ts @@ -0,0 +1,172 @@ +import { useCallback } from 'react' +import { useMutation } from '@apollo/client' + +import { UploadFile } from '@island.is/island-ui/core' +import { CaseFileCategory } from '@island.is/judicial-system/types' +import { + CreateFileMutationDocument, + CreateFileMutationMutation, + CreateFileMutationMutationVariables, + CreatePresignedPostMutationDocument, + CreatePresignedPostMutationMutation, + CreatePresignedPostMutationMutationVariables, + DeleteFileMutationDocument, + DeleteFileMutationMutation, + DeleteFileMutationMutationVariables, + PresignedPost, +} from '@island.is/judicial-system-web/src/graphql/schema' + +const createFormData = (presignedPost: PresignedPost, file: File): FormData => { + const formData = new FormData() + Object.keys(presignedPost.fields).forEach((key) => + formData.append(key, presignedPost.fields[key]), + ) + formData.append('file', file) + + return formData +} + +const uploadToS3 = ( + file: File, + presignedPost: PresignedPost, + onProgress: (percent: number) => void, +) => { + const promise = new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.withCredentials = true + request.responseType = 'json' + + request.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + onProgress((event.loaded / event.total) * 100) + } + }) + + request.upload.addEventListener('error', (event) => { + if (event.lengthComputable) { + reject() + } + }) + + request.addEventListener('load', () => { + if (request.status >= 200 && request.status < 300) { + resolve(file) + } else { + reject() + } + }) + + request.open('POST', presignedPost.url) + request.send(createFormData(presignedPost, file)) + }) + return promise +} + +export const useS3UploadV2 = ( + caseId: string, + category: CaseFileCategory, + policeCaseNumber: string, +) => { + const [createPresignedMutation] = useMutation< + CreatePresignedPostMutationMutation, + CreatePresignedPostMutationMutationVariables + >(CreatePresignedPostMutationDocument) + const [addFileToCaseMutation] = useMutation< + CreateFileMutationMutation, + CreateFileMutationMutationVariables + >(CreateFileMutationDocument) + + const [deleteFileMutation] = useMutation< + DeleteFileMutationMutation, + DeleteFileMutationMutationVariables + >(DeleteFileMutationDocument) + + const upload = useCallback( + (files: Array<[File, string]>, updateFile: (file: UploadFile) => void) => { + files.forEach(async ([file, id]) => { + try { + const data = await createPresignedMutation({ + variables: { + input: { + caseId, + fileName: file.name.normalize(), + type: file.type ?? '', + }, + }, + }) + if (!data.data?.createPresignedPost.fields?.key) { + throw Error('failed to get presigned post') + } + + const presignedPost = data.data.createPresignedPost + await uploadToS3(file, presignedPost, (percent) => { + updateFile({ + id, + name: file.name, + percent, + status: 'uploading', + }) + }) + + const data2 = await addFileToCaseMutation({ + variables: { + input: { + caseId, + type: file.type, + key: presignedPost.fields.key, + size: file.size, + category: category, + policeCaseNumber: policeCaseNumber, + }, + }, + }) + if (!data2.data?.createFile.id) { + throw Error('failed to add file to case') + } + + updateFile({ + name: file.name, + percent: 100, + status: 'done', + // We need to set the id so we are able to delete the file later + id: data2.data.createFile.id, + }) + } catch (e) { + updateFile({ + id: id, + name: file.name, + status: 'error', + }) + } + }) + }, + [ + createPresignedMutation, + caseId, + addFileToCaseMutation, + category, + policeCaseNumber, + ], + ) + + const remove = useCallback( + (fileId) => { + return deleteFileMutation({ + variables: { + input: { + caseId: caseId, + id: fileId, + }, + }, + optimisticResponse: { + deleteFile: { success: true, __typename: 'DeleteFileResponse' }, + }, + }) + }, + [deleteFileMutation, caseId], + ) + + return { upload, remove } +} + +export default useS3UploadV2 diff --git a/apps/judicial-system/web/src/utils/hooks/useSections/index.ts b/apps/judicial-system/web/src/utils/hooks/useSections/index.ts index 6b703e8ccae8..35af80912b89 100644 --- a/apps/judicial-system/web/src/utils/hooks/useSections/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useSections/index.ts @@ -282,6 +282,19 @@ const useSections = () => { ? `${constants.INDICTMENTS_CASE_FILES_ROUTE}/${id}` : undefined, }, + { + type: 'SUB_SECTION', + name: capitalize( + formatMessage( + sections.indictmentCaseProsecutorSection.policeCaseFiles, + ), + ), + href: + isDefendantStepValidForSidebarIndictments(workingCase) && + isProcessingStepValidIndictments(workingCase) + ? `${constants.INDICTMENTS_POLICE_CASE_FILES_ROUTE}/${id}` + : undefined, + }, { type: 'SUB_SECTION', name: capitalize( diff --git a/libs/judicial-system/consts/src/lib/consts.ts b/libs/judicial-system/consts/src/lib/consts.ts index b32b9590f787..f7fe7f673342 100644 --- a/libs/judicial-system/consts/src/lib/consts.ts +++ b/libs/judicial-system/consts/src/lib/consts.ts @@ -199,6 +199,7 @@ export const INVESTIGATION_CASE_POLICE_CONFIRMATION_ROUTE = export const INDICTMENTS_DEFENDANT_ROUTE = '/krafa/akaera/akaerdi' export const INDICTMENTS_PROCESSING_ROUTE = '/krafa/akaera/malsmedferd' export const INDICTMENTS_CASE_FILES_ROUTE = '/krafa/akaera/domskjol' +export const INDICTMENTS_POLICE_CASE_FILES_ROUTE = '/krafa/akaera/malsgogn' export const INDICTMENTS_OVERVIEW_ROUTE = '/krafa/akaera/stadfesta' /* PROSECUTOR ROUTES END */