From 0e0892fd43c730090e46492293e4b6e711559cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Sch=C3=BCtte?= Date: Fri, 12 Jan 2024 19:04:56 -0500 Subject: [PATCH] change gql mutation uploads to use fetch (#146) * make reusuable program path func * add form helper * abstract fetch with options * use react query fetch instead of apollo gql * account for FileList * remove credentials include * add auth and form data creation fix --------- Co-authored-by: Ciaran Schutte --- .../submission/components/ProgramMenu.tsx | 14 +++--- .../[shortName]/clinical-submission/page.tsx | 50 ++++++++++++------- .../[shortName]/sample-registration/page.tsx | 41 +++++++-------- src/global/constants.ts | 4 ++ src/global/utils/form.ts | 37 ++++++++++++++ src/global/utils/index.tsx | 1 + src/global/utils/url.ts | 23 +++++++++ 7 files changed, 124 insertions(+), 46 deletions(-) create mode 100644 src/global/utils/form.ts create mode 100644 src/global/utils/url.ts diff --git a/src/app/(post-login)/submission/components/ProgramMenu.tsx b/src/app/(post-login)/submission/components/ProgramMenu.tsx index ec97529e..32351f3b 100644 --- a/src/app/(post-login)/submission/components/ProgramMenu.tsx +++ b/src/app/(post-login)/submission/components/ProgramMenu.tsx @@ -33,9 +33,8 @@ import { PROGRAM_DASHBOARD_PATH, PROGRAM_MANAGE_PATH, PROGRAM_SAMPLE_REGISTRATION_PATH, - PROGRAM_SHORT_NAME_PATH, } from '@/global/constants'; -import { notNull } from '@/global/utils'; +import { getProgramPath, notNull } from '@/global/utils'; import { css } from '@/lib/emotion'; import { Icon, MenuItem } from '@icgc-argo/uikit'; import orderBy from 'lodash/orderBy'; @@ -246,7 +245,6 @@ const MenuContent = ({ programName }: { programName: string }) => { const { egoJwt } = useAuthContext(); const pathname = usePathname(); const pathnameLastSegment = pathname.split('/').at(-1); - const getProgramPath = (path: string) => path.replace(PROGRAM_SHORT_NAME_PATH, programName); const { isDisabled: isSubmissionSystemDisabled } = useSubmissionSystemStatus(); @@ -279,15 +277,15 @@ const MenuContent = ({ programName }: { programName: string }) => { return ( <> {/** Dashboard */} - + {/** Register Samples */} {userCanSubmitData && renderDataSubmissionLinks( - getProgramPath(PROGRAM_SAMPLE_REGISTRATION_PATH), - getProgramPath(PROGRAM_CLINICAL_SUBMISSION_PATH), + getProgramPath(PROGRAM_SAMPLE_REGISTRATION_PATH, programName), + getProgramPath(PROGRAM_CLINICAL_SUBMISSION_PATH, programName), registrationStatusIcon, isSubmissionSystemDisabled, pathnameLastSegment, @@ -296,7 +294,7 @@ const MenuContent = ({ programName }: { programName: string }) => { {/** Submitted Data */} {userCanViewData && ( - + { {/** Manage Program */} {userCanManageProgram && ( - + { const pathname = usePathname(); const { setGlobalLoading } = useGlobalLoader(); const toaster = useToaster(); + const { CLINICAL_API_ROOT } = useAppConfigContext(); + const { egoJwt } = useAuthContext(); useEffect(() => { const defaultQuery = '?tab=donor'; @@ -100,28 +110,32 @@ const ClinicalSubmission = ({ shortName }: { shortName: string }) => { }); // mutations - const [clearClinicalSubmission] = useMutation(CLEAR_CLINICAL_SUBMISSION); - const [uploadClinicalSubmission] = useMutation(UPLOAD_CLINICAL_SUBMISSION_MUTATION, { - onError: () => { - commonToaster.unknownError(); - }, - }); + const [clearClinicalSubmission] = useGQLMutation(CLEAR_CLINICAL_SUBMISSION); - const handleSubmissionFilesUpload = (files: FileList) => - uploadClinicalSubmission({ - variables: { - programShortName: shortName, - files, + const uploadClinicalSubmission = useMutation( + (formData) => { + const url = urlJoin(CLINICAL_API_ROOT, getProgramPath(UPLOAD_CLINICAL_DATA, shortName)); + return uploadFileRequest(url, formData, egoJwt); + }, + { + onError: () => { + commonToaster.unknownError(); }, - }); + }, + ); + + const handleSubmissionFilesUpload = (files: FileList) => { + const fileFormData = createFileFormData(files, 'clinicalFiles'); + return uploadClinicalSubmission.mutate(fileFormData); + }; - const [validateSubmission] = useMutation(VALIDATE_SUBMISSION_MUTATION, { + const [validateSubmission] = useGQLMutation(VALIDATE_SUBMISSION_MUTATION, { onCompleted: () => { //setSelectedClinicalEntityType(defaultClinicalEntityType); }, }); - const [signOffSubmission] = useMutation(SIGN_OFF_SUBMISSION_MUTATION); + const [signOffSubmission] = useGQLMutation(SIGN_OFF_SUBMISSION_MUTATION); const { isDisabled: isSubmissionSystemDisabled } = useSubmissionSystemStatus(); diff --git a/src/app/(post-login)/submission/program/[shortName]/sample-registration/page.tsx b/src/app/(post-login)/submission/program/[shortName]/sample-registration/page.tsx index 467d3241..45b8d92f 100644 --- a/src/app/(post-login)/submission/program/[shortName]/sample-registration/page.tsx +++ b/src/app/(post-login)/submission/program/[shortName]/sample-registration/page.tsx @@ -28,15 +28,17 @@ import { BreadcrumbTitle, HelpLink, PageHeader } from '@/app/components/PageHead import CLEAR_CLINICAL_REGISTRATION_MUTATION from '@/app/gql/clinical/CLEAR_CLINICAL_REGISTRATION_MUTATION'; import CLINICAL_SCHEMA_VERSION from '@/app/gql/clinical/CLINICAL_SCHEMA_VERSION'; import GET_REGISTRATION_QUERY from '@/app/gql/clinical/GET_REGISTRATION_QUERY'; -import UPLOAD_REGISTRATION_MUTATION from '@/app/gql/gateway/UPLOAD_REGISTRATION_MUTATION'; import { useAppConfigContext } from '@/app/hooks/AppProvider'; +import { useAuthContext } from '@/app/hooks/AuthProvider'; import { useToaster } from '@/app/hooks/ToastProvider'; import { useClinicalQuery } from '@/app/hooks/useApolloQuery'; import useCommonToasters from '@/app/hooks/useCommonToasters'; import { useSubmissionSystemStatus } from '@/app/hooks/useSubmissionSystemStatus'; -import { notNull } from '@/global/utils'; +import { UPLOAD_REGISTRATION } from '@/global/constants'; +import { getProgramPath, notNull } from '@/global/utils'; +import { createFileFormData, uploadFileRequest } from '@/global/utils/form'; import { css } from '@/lib/emotion'; -import { useMutation, useQuery } from '@apollo/client'; +import { useMutation as useGQLMutation, useQuery } from '@apollo/client'; import { BUTTON_SIZES, BUTTON_VARIANTS, @@ -47,6 +49,7 @@ import { } from '@icgc-argo/uikit'; import { get } from 'lodash'; import { useState } from 'react'; +import { useMutation } from 'react-query'; import urlJoin from 'url-join'; import FileError from '../../../../../components/FileError'; import FilePreview from './components/FilePreview'; @@ -62,6 +65,8 @@ const Register = ({ shortName }: { shortName: string }) => { variables: { shortName }, }); + const { egoJwt } = useAuthContext(); + // get dictionary version const latestDictionaryResponse = useClinicalQuery(CLINICAL_SCHEMA_VERSION); const { loading: isLoadingDictVersion, data: dictData } = latestDictionaryResponse; @@ -73,7 +78,7 @@ const Register = ({ shortName }: { shortName: string }) => { const commonToaster = useCommonToasters(); // docs url - const { DOCS_URL_ROOT } = useAppConfigContext(); + const { DOCS_URL_ROOT, CLINICAL_API_ROOT } = useAppConfigContext(); const helpUrl = urlJoin(DOCS_URL_ROOT, '/docs/submission/registering-samples'); // modal state @@ -96,33 +101,29 @@ const Register = ({ shortName }: { shortName: string }) => { const registrationId = get(clinicalRegistration, 'id', '') || ''; // handlers - const [uploadFile, { loading: isUploading }] = useMutation( - UPLOAD_REGISTRATION_MUTATION, - + const uploadFile = useMutation( + (formData) => { + const url = urlJoin(CLINICAL_API_ROOT, getProgramPath(UPLOAD_REGISTRATION, shortName)); + return uploadFileRequest(url, formData, egoJwt); + }, { - onError: (e) => { + onError: () => { commonToaster.unknownError(); }, }, ); - const [uploadClinicalSubmission, mutationStatus] = useMutation(UPLOAD_REGISTRATION_MUTATION, { - onError: (e) => { - commonToaster.unknownError(); - }, - }); - - const handleUpload = (file: File) => - uploadClinicalSubmission({ - variables: { shortName, registrationFile: file }, - }); + const handleUpload = (file: File) => { + const fileFormData = createFileFormData(file, 'registrationFile'); + return uploadFile.mutate(fileFormData); + }; const handleRegister = () => { setShowModal((state) => !state); }; // file preview clear - const [clearRegistration] = useMutation(CLEAR_CLINICAL_REGISTRATION_MUTATION); + const [clearRegistration] = useGQLMutation(CLEAR_CLINICAL_REGISTRATION_MUTATION); const handleClearClick = async () => { if (clinicalRegistration?.id == null) { refetch(); @@ -192,7 +193,7 @@ const Register = ({ shortName }: { shortName: string }) => { diff --git a/src/global/constants.ts b/src/global/constants.ts index 0379e9de..b28c7f6f 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -35,4 +35,8 @@ export const PROGRAM_CLINICAL_SUBMISSION_PATH = `${SUBMISSION_PATH}/program/${PR export const PROGRAM_CLINICAL_DATA_PATH = `${SUBMISSION_PATH}/program/${PROGRAM_SHORT_NAME_PATH}/clinical-data`; export const CREATE_PROGRAM_PAGE_PATH = `${SUBMISSION_PATH}/program/create`; +// Upload paths +export const UPLOAD_REGISTRATION = `/clinical/api/submission/program/${PROGRAM_SHORT_NAME_PATH}/registration`; +export const UPLOAD_CLINICAL_DATA = `/clinical/api/submission/program/${PROGRAM_SHORT_NAME_PATH}/clinical/submissionUpload`; + export const CLINICAL_TEMPLATE_PATH = '/clinical/proxy/template'; diff --git a/src/global/utils/form.ts b/src/global/utils/form.ts new file mode 100644 index 00000000..16c8f0fe --- /dev/null +++ b/src/global/utils/form.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export const createFileFormData = (upload, uploadName) => { + const formData = new FormData(); + + const filesToAdd = !Array.isArray(upload) ? [upload] : upload; + for (const [i, file] of filesToAdd.entries()) { + formData.append(uploadName || `file_${i}`, file); + } + return formData; +}; + +export const uploadFileRequest = (url, body, jwt) => { + const options: RequestInit = { + headers: { accept: '*/*', authorization: `Bearer ${jwt}` }, + method: 'POST', + body, + }; + return fetch(url, options); +}; diff --git a/src/global/utils/index.tsx b/src/global/utils/index.tsx index 4f2b6d94..8139ffbc 100644 --- a/src/global/utils/index.tsx +++ b/src/global/utils/index.tsx @@ -19,3 +19,4 @@ export * from './clinical'; export * from './misc'; export * from './types'; +export * from './url'; diff --git a/src/global/utils/url.ts b/src/global/utils/url.ts new file mode 100644 index 00000000..4375b96f --- /dev/null +++ b/src/global/utils/url.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { PROGRAM_SHORT_NAME_PATH } from '../constants'; + +export const getProgramPath = (path: string, programName: string) => + path.replace(PROGRAM_SHORT_NAME_PATH, programName);