From e309eb799c07a7aa7b691803c134f583db0a6395 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Thu, 4 Jul 2024 17:16:21 +0100 Subject: [PATCH] fix(frontend): prevent project generation if api failure --- src/frontend/src/api/CreateProjectService.ts | 160 ++++++++++-------- .../createnewproject/SplitTasks.tsx | 10 +- .../src/store/slices/CreateProjectSlice.ts | 25 ++- .../src/store/types/ICreateProject.ts | 10 +- 4 files changed, 104 insertions(+), 101 deletions(-) diff --git a/src/frontend/src/api/CreateProjectService.ts b/src/frontend/src/api/CreateProjectService.ts index 3129d62a7b..7ed097d918 100755 --- a/src/frontend/src/api/CreateProjectService.ts +++ b/src/frontend/src/api/CreateProjectService.ts @@ -21,72 +21,69 @@ const CreateProjectService: Function = ( dispatch(CreateProjectActions.CreateProjectLoading(true)); dispatch(CommonActions.SetLoading(true)); - const postCreateProjectDetails = async (url, projectData, taskAreaGeojson, formUpload) => { - try { - // Create project - const postNewProjectDetails = await axios.post(url, projectData); - const resp: ProjectDetailsModel = postNewProjectDetails.data; - await dispatch(CreateProjectActions.PostProjectDetails(resp)); - - // Submit task boundaries - await dispatch( - UploadTaskAreasService( - `${import.meta.env.VITE_API_URL}/projects/${resp.id}/upload-task-boundaries`, - taskAreaGeojson, - ), - ); - - dispatch( - CommonActions.SetSnackBar({ - open: true, - message: 'Project Successfully Created Now Generating QR For Project', - variant: 'success', - duration: 2000, - }), + try { + // Create project + const postNewProjectDetails = await API.post(url, projectData); + const projectCreateResp: ProjectDetailsModel = postNewProjectDetails.data; + await dispatch(CreateProjectActions.PostProjectDetails(projectCreateResp)); + + if (projectCreateResp.status >= 300) { + throw new Error(`Request failed with status ${projectCreateResp.status}`); + } + const projectId = projectCreateResp.id; + + // Submit task boundaries + await dispatch( + UploadTaskAreasService( + `${import.meta.env.VITE_API_URL}/projects/${projectId}/upload-task-boundaries`, + taskAreaGeojson, + ), + ); + + // Upload data extract + let extractResponse + if (isOsmExtract) { + // Generated extract from raw-data-api + extractResponse = await API.get( + `${import.meta.env.VITE_API_URL}/projects/data-extract-url/?project_id=${projectId}&url=${projectData.data_extract_url}`, ); - - if (isOsmExtract) { - // Upload data extract generated from raw-data-api - const response = await axios.get( - `${import.meta.env.VITE_API_URL}/projects/data-extract-url/?project_id=${resp.id}&url=${ - projectData.data_extract_url - }`, - ); - } else if (dataExtractFile) { - // Upload custom data extract from user - const dataExtractFormData = new FormData(); - dataExtractFormData.append('custom_extract_file', dataExtractFile); - const response = await axios.post( - `${import.meta.env.VITE_API_URL}/projects/upload-custom-extract/?project_id=${resp.id}`, - dataExtractFormData, - ); - } - - // Generate QR codes - await dispatch( - GenerateProjectQRService( - `${import.meta.env.VITE_API_URL}/projects/${resp.id}/generate-project-data`, - projectData, - formUpload, - ), + } else if (dataExtractFile) { + // Custom data extract from user + const dataExtractFormData = new FormData(); + dataExtractFormData.append('custom_extract_file', dataExtractFile); + extractResponse = await API.post( + `${import.meta.env.VITE_API_URL}/projects/upload-custom-extract/?project_id=${projectId}`, + dataExtractFormData, ); - } catch (error: any) { - // Added Snackbar toast for error message - dispatch( - CommonActions.SetSnackBar({ - open: true, - message: JSON.stringify(error?.response?.data?.detail) || 'Something went wrong.', - variant: 'error', - duration: 2000, - }), - ); - dispatch(CreateProjectActions.CreateProjectLoading(false)); - } finally { - dispatch(CommonActions.SetLoading(false)); } - }; + if (extractResponse.status >= 300) { + throw new Error(`Request failed with status ${extractResponse.status}`); + } - await postCreateProjectDetails(url, projectData, taskAreaGeojson, formUpload); + // Generate project files + await dispatch( + GenerateProjectFilesService( + `${import.meta.env.VITE_API_URL}/projects/${projectId}/generate-project-data`, + projectData, + formUpload, + ), + ); + + dispatch(CreateProjectActions.CreateProjectLoading(false)); + } catch (error: any) { + await dispatch(CreateProjectActions.GenerateProjectError(true)); + dispatch( + CommonActions.SetSnackBar({ + open: true, + message: JSON.stringify(error?.response?.data?.detail) || 'Something went wrong.', + variant: 'error', + duration: 2000, + }), + ); + dispatch(CreateProjectActions.CreateProjectLoading(false)); + } finally { + dispatch(CommonActions.SetLoading(false)); + } }; }; @@ -107,6 +104,7 @@ const FormCategoryService: Function = (url: string) => { await getFormCategoryList(url); }; }; + const UploadTaskAreasService: Function = (url: string, filePayload: any, projectData: any) => { return async (dispatch) => { dispatch(CreateProjectActions.UploadAreaLoading(true)); @@ -119,10 +117,15 @@ const UploadTaskAreasService: Function = (url: string, filePayload: any, project 'Content-Type': 'multipart/form-data', }, }); - // const resp: UploadAreaDetailsModel = postNewProjectDetails.data; - await dispatch(CreateProjectActions.UploadAreaLoading(false)); - await dispatch(CreateProjectActions.PostUploadAreaSuccess(postNewProjectDetails.data)); + + if (postNewProjectDetails.status >= 200 && postNewProjectDetails.status < 300) { + await dispatch(CreateProjectActions.UploadAreaLoading(false)); + await dispatch(CreateProjectActions.PostUploadAreaSuccess(postNewProjectDetails.data)); + } else { + throw new Error(`Request failed with status ${postNewProjectDetails.status}`); + } } catch (error: any) { + await dispatch(CreateProjectActions.GenerateProjectError(true)); dispatch( CommonActions.SetSnackBar({ open: true, @@ -138,34 +141,40 @@ const UploadTaskAreasService: Function = (url: string, filePayload: any, project await postUploadArea(url, filePayload); }; }; -const GenerateProjectQRService: Function = (url: string, projectData: any, formUpload: any) => { + +const GenerateProjectFilesService: Function = (url: string, projectData: any, formUpload: any) => { return async (dispatch) => { - dispatch(CreateProjectActions.GenerateProjectQRLoading(true)); + dispatch(CreateProjectActions.GenerateProjectLoading(true)); dispatch(CommonActions.SetLoading(true)); const postUploadArea = async (url, projectData: any, formUpload) => { try { - let postNewProjectDetails; + let response; if (projectData.form_ways === 'custom_form') { // TODO move form upload to a separate service / endpoint? const generateApiFormData = new FormData(); generateApiFormData.append('xls_form_upload', formUpload); - postNewProjectDetails = await axios.post(url, generateApiFormData, { + response = await axios.post(url, generateApiFormData, { headers: { 'Content-Type': 'multipart/form-data', }, }); } else { - postNewProjectDetails = await axios.post(url, {}); + response = await axios.post(url, {}); + } + + if (response.status > 300) { + throw new Error(`Request failed with status ${response.status}`); } - const resp: string = postNewProjectDetails.data; - await dispatch(CreateProjectActions.GenerateProjectQRLoading(false)); + await dispatch(CreateProjectActions.GenerateProjectLoading(false)); dispatch(CommonActions.SetLoading(false)); - await dispatch(CreateProjectActions.GenerateProjectQRSuccess(resp)); + // Trigger the watcher and redirect after success + await dispatch(CreateProjectActions.GenerateProjectSuccess(true)); } catch (error: any) { dispatch(CommonActions.SetLoading(false)); + await dispatch(CreateProjectActions.GenerateProjectError(true)); dispatch( CommonActions.SetSnackBar({ open: true, @@ -174,7 +183,7 @@ const GenerateProjectQRService: Function = (url: string, projectData: any, formU duration: 2000, }), ); - dispatch(CreateProjectActions.GenerateProjectQRLoading(false)); + dispatch(CreateProjectActions.GenerateProjectLoading(false)); } }; @@ -333,6 +342,7 @@ const PatchProjectDetails: Function = (url: string, projectData: any) => { await patchProjectDetails(url, projectData); }; }; + const PostFormUpdate: Function = (url: string, projectData: any) => { return async (dispatch) => { dispatch(CreateProjectActions.SetPostFormUpdateLoading(true)); @@ -492,7 +502,7 @@ export { UploadTaskAreasService, CreateProjectService, FormCategoryService, - GenerateProjectQRService, + GenerateProjectFilesService, OrganisationService, GetDividedTaskFromGeojson, TaskSplittingPreviewService, diff --git a/src/frontend/src/components/createnewproject/SplitTasks.tsx b/src/frontend/src/components/createnewproject/SplitTasks.tsx index 4a5875d96e..8bb3ff7893 100644 --- a/src/frontend/src/components/createnewproject/SplitTasks.tsx +++ b/src/frontend/src/components/createnewproject/SplitTasks.tsx @@ -31,7 +31,8 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload const projectDetails = useAppSelector((state) => state.createproject.projectDetails); const dataExtractGeojson = useAppSelector((state) => state.createproject.dataExtractGeojson); - const generateQrSuccess = useAppSelector((state) => state.createproject.generateQrSuccess); + const generateProjectSuccess = useAppSelector((state) => state.createproject.generateProjectSuccess); + const generateProjectError = useAppSelector((state) => state.createproject.generateProjectError); const projectDetailsResponse = useAppSelector((state) => state.createproject.projectDetailsResponse); const dividedTaskGeojson = useAppSelector((state) => state.createproject.dividedTaskGeojson); const projectDetailsLoading = useAppSelector((state) => state.createproject.projectDetailsLoading); @@ -191,12 +192,12 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const handleQRGeneration = async () => { - if (generateQrSuccess) { + if (!generateProjectError && generateProjectSuccess) { const projectId = projectDetailsResponse?.id; dispatch( CommonActions.SetSnackBar({ open: true, - message: 'QR Generation Completed. Redirecting...', + message: 'Project Generation Completed. Redirecting...', variant: 'success', duration: 2000, }), @@ -205,7 +206,6 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload // Add 5-second delay to allow backend Entity generation to catch up await delay(5000); dispatch(CreateProjectActions.CreateProjectLoading(false)); - dispatch(CreateProjectActions.SetGenerateProjectQRSuccess(null)); navigate(`/project/${projectId}`); dispatch(CreateProjectActions.ClearCreateProjectFormData()); dispatch(CreateProjectActions.SetCanSwitchCreateProjectSteps(false)); @@ -213,7 +213,7 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload }; handleQRGeneration(); - }, [generateQrSuccess]); + }, [generateProjectSuccess]); const renderTraceback = (errorText: string) => { if (!errorText) { diff --git a/src/frontend/src/store/slices/CreateProjectSlice.ts b/src/frontend/src/store/slices/CreateProjectSlice.ts index 619be23fdc..2cd9745ba5 100755 --- a/src/frontend/src/store/slices/CreateProjectSlice.ts +++ b/src/frontend/src/store/slices/CreateProjectSlice.ts @@ -28,10 +28,11 @@ export const initialState: CreateProjectStateTypes = { projectAreaLoading: false, formCategoryList: [], formCategoryLoading: false, - generateQrLoading: false, + generateProjectLoading: false, + generateProjectSuccess: false, + generateProjectError: false, organisationList: [], organisationListLoading: false, - generateQrSuccess: null, createProjectStep: 1, dividedTaskLoading: false, dividedTaskGeojson: null, @@ -110,8 +111,14 @@ const CreateProject = createSlice({ SetIndividualProjectDetailsData(state, action) { state.projectDetails = action.payload; }, - GenerateProjectQRLoading(state, action) { - state.generateQrLoading = action.payload; + GenerateProjectLoading(state, action) { + state.generateProjectLoading = action.payload; + }, + GenerateProjectSuccess(state, action) { + state.generateProjectSuccess = action.payload; + }, + GenerateProjectError(state, action) { + state.generateProjectError = action.payload; }, GetOrganisationList(state, action) { state.organisationList = action.payload; @@ -119,16 +126,6 @@ const CreateProject = createSlice({ GetOrganisationListLoading(state, action) { state.organisationListLoading = action.payload; }, - GenerateProjectQRSuccess(state, action) { - if (action.payload.status === 'SUCCESS') { - state.generateQrSuccess = null; - } else { - state.generateQrSuccess = action.payload; - } - }, - SetGenerateProjectQRSuccess(state, action) { - state.generateQrSuccess = action.payload; - }, SetCreateProjectFormStep(state, action) { state.createProjectStep = action.payload; }, diff --git a/src/frontend/src/store/types/ICreateProject.ts b/src/frontend/src/store/types/ICreateProject.ts index 013b791c40..7ea894ce12 100644 --- a/src/frontend/src/store/types/ICreateProject.ts +++ b/src/frontend/src/store/types/ICreateProject.ts @@ -11,10 +11,11 @@ export type CreateProjectStateTypes = { projectAreaLoading: boolean; formCategoryList: FormCategoryListTypes[] | []; formCategoryLoading: boolean; - generateQrLoading: boolean; + generateProjectLoading: boolean; + generateProjectSuccess: boolean; + generateProjectError: boolean; organisationList: OrganisationListTypes[]; organisationListLoading: boolean; - generateQrSuccess: GenerateQrSuccessTypes | null; createProjectStep: number; dividedTaskLoading: boolean; dividedTaskGeojson: null | GeoJSONFeatureTypes; @@ -129,11 +130,6 @@ export type FormCategoryListTypes = { title: string; }; -export type GenerateQrSuccessTypes = { - Message: string; - task_id: string; -}; - export type OrganisationListTypes = { logo: string; id: number;