Skip to content

Commit

Permalink
:feat: Wire up questionnaire upload to hub api
Browse files Browse the repository at this point in the history
Signed-off-by: ibolton336 <[email protected]>
  • Loading branch information
ibolton336 committed Sep 8, 2023
1 parent 94293cc commit 73f5571
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 52 deletions.
5 changes: 5 additions & 0 deletions client/src/app/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,11 @@ export const getQuestionnaireById = (
): Promise<Questionnaire> =>
axios.get(`${QUESTIONNAIRES}/id/${id}`).then((response) => response.data);

export const createQuestionnaire = (
obj: Questionnaire
): Promise<Questionnaire> =>
axios.post(`${QUESTIONNAIRES}`, obj).then((response) => response.data);

// TODO: The update handlers in hub don't return any content (success is a response code
// TODO: of 204 - NoContext) ... the return type does not make sense.
export const updateQuestionnaire = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,9 @@ const AssessmentSettings: React.FC = () => {
>(null);
const [questionnaireToDelete, setQuestionnaireToDelete] =
React.useState<Questionnaire>();

const tableControls = useLocalTableControls({
idProperty: "id",
items: questionnaires,
items: questionnaires || [],
columnNames: {
required: "Required",
name: "Name",
Expand All @@ -128,9 +127,9 @@ const AssessmentSettings: React.FC = () => {
},
],
sortableColumns: ["name", "dateImported"],
getSortValues: (assessment) => ({
name: assessment.name || "",
dateImported: assessment.dateImported || "",
getSortValues: (questionnaire) => ({
name: questionnaire.name || "",
dateImported: questionnaire.dateImported || "",
}),
initialSort: { columnKey: "name", direction: "asc" },
hasPagination: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { AxiosResponse } from "axios";
import { AxiosError, AxiosResponse } from "axios";
import { useTranslation } from "react-i18next";
import * as yup from "yup";

Expand All @@ -16,27 +16,20 @@ import {

import { HookFormPFGroupController } from "@app/components/HookFormPFFields";
import { useForm } from "react-hook-form";
import { FileLoadError, IReadFile } from "@app/api/models";
import { Questionnaire } from "@app/api/models";
import { yupResolver } from "@hookform/resolvers/yup";
import { useCreateFileMutation } from "@app/queries/targets";
import { useCreateQuestionnaireMutation } from "@app/queries/questionnaires";
import jsYaml from "js-yaml";
import { NotificationsContext } from "@app/components/NotificationsContext";
import { getAxiosErrorMessage } from "@app/utils/utils";

export interface ImportQuestionnaireFormProps {
onSaved: (response?: AxiosResponse) => void;
onSaved: (response?: Questionnaire) => void;
}
export interface ImportQuestionnaireFormValues {
yamlFile: IReadFile;
yamlFile: string;
}

export const yamlFileSchema: yup.SchemaOf<IReadFile> = yup.object({
fileName: yup.string().required(),
fullFile: yup.mixed<File>(),
loadError: yup.mixed<FileLoadError>(),
loadPercentage: yup.number(),
loadResult: yup.mixed<"danger" | "success" | undefined>(),
data: yup.string(),
responseID: yup.number(),
});

export const ImportQuestionnaireForm: React.FC<
ImportQuestionnaireFormProps
> = ({ onSaved }) => {
Expand All @@ -47,7 +40,7 @@ export const ImportQuestionnaireForm: React.FC<
const validationSchema: yup.SchemaOf<ImportQuestionnaireFormValues> = yup
.object()
.shape({
yamlFile: yamlFileSchema,
yamlFile: yup.string().required(),
});
const methods = useForm<ImportQuestionnaireFormValues>({
resolver: yupResolver(validationSchema),
Expand All @@ -57,37 +50,81 @@ export const ImportQuestionnaireForm: React.FC<
const {
handleSubmit,
formState: { isSubmitting, isValidating, isValid, isDirty },
getValues,
setValue,
control,
watch,
setFocus,
clearErrors,
trigger,
reset,
} = methods;

const { mutateAsync: createYamlFileAsync } = useCreateFileMutation();
const onHandleSuccessfullQuestionnaireCreation = (
response: Questionnaire
) => {
onSaved(response);
pushNotification({
title: t("toastr.success.questionnaireCreated"),
variant: "success",
});
onSaved();
};

const handleFileUpload = async (file: File) => {
setFilename(file.name);
const formFile = new FormData();
formFile.append("file", file);
const onHandleFailedQuestionnaireCreation = (error: AxiosError) => {
pushNotification({
title: getAxiosErrorMessage(error),
variant: "danger",
});
};

const { mutate: createQuestionnaire } = useCreateQuestionnaireMutation(
onHandleSuccessfullQuestionnaireCreation,
onHandleFailedQuestionnaireCreation
);

const newYamlFile: IReadFile = {
fileName: file.name,
fullFile: file,
};
const { pushNotification } = React.useContext(NotificationsContext);

return createYamlFileAsync({
formData: formFile,
file: newYamlFile,
});
const convertYamlToJson = (yamlString: string) => {
try {
const jsonData = jsYaml.load(yamlString);
return jsonData;
} catch (error) {
pushNotification({
title: "Failed",
message: getAxiosErrorMessage(error as AxiosError),
variant: "danger",
timeout: 30000,
});
}
};

function isQuestionnaire(data: any): data is Questionnaire {
return (
typeof data === "object" &&
data !== null &&
"name" in data &&
"description" in data
);
}

const onSubmit = (values: ImportQuestionnaireFormValues) => {
console.log("values", values);
onSaved();
if (values.yamlFile) {
try {
const jsonData = convertYamlToJson(values.yamlFile);

if (isQuestionnaire(jsonData)) {
const questionnaireData = jsonData as Questionnaire;

createQuestionnaire(questionnaireData);
} else {
console.error("Invalid JSON data.");
}
} catch (error) {
pushNotification({
title: "Failed",
message: getAxiosErrorMessage(error as AxiosError),
variant: "danger",
timeout: 30000,
});
}
}
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
Expand Down Expand Up @@ -122,20 +159,40 @@ export const ImportQuestionnaireForm: React.FC<
}}
validated={isFileRejected || error ? "error" : "default"}
onFileInputChange={async (_, file) => {
console.log("uploading file", file);
//TODO: handle new api here. This is just a placeholder.
try {
await handleFileUpload(file);
if (!file) {
console.error("No file selected.");
return;
}

const reader = new FileReader();

reader.onload = (e) => {
try {
const yamlContent = e?.target?.result as string;
onChange(yamlContent);
setFilename(file.name);
} catch (error) {
console.error("Error reading YAML file:", error);
}
};

reader.readAsText(file);
setFocus(name);
clearErrors(name);
trigger(name);
} catch (err) {
//Handle new api error here
pushNotification({
title: "Failed",
message: getAxiosErrorMessage(err as AxiosError),
variant: "danger",
timeout: 30000,
});
}
}}
onClearClick={() => {
//TODO
console.log("clearing file");
onChange("");
setFilename("");
}}
browseButtonText="Upload"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Sample Questionnaire
description: This is a sample questionnaire in YAML format
revision: 1
required: true
sections:
- order: 1
name: Section 1
questions:
- order: 1
text: What is your favorite color?
explanation: Please select your favorite color.
includeFor:
- category: Category1
tag: Tag1
excludeFor: []
answers:
- order: 1
text: Red
risk: red
rationale: Red is a nice color.
mitigation: No mitigation needed.
applyTags: []
autoAnswerFor: []
selected: false
autoAnswered: false
- order: 2
text: Blue
risk: green
rationale: Blue is a calming color.
mitigation: No mitigation needed.
applyTags: []
autoAnswerFor: []
selected: false
autoAnswered: false
thresholds:
red: 5
yellow: 10
unknown: 15
riskMessages:
red: High risk
yellow: Medium risk
green: Low risk
unknown: Unknown risk
29 changes: 27 additions & 2 deletions client/src/app/queries/questionnaires.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { AxiosError, AxiosResponse } from "axios";

import {
createQuestionnaire,
deleteQuestionnaire,
getQuestionnaireById,
getQuestionnaires,
Expand All @@ -18,7 +19,6 @@ export const useFetchQuestionnaires = () => {
queryFn: getQuestionnaires,
onError: (error: AxiosError) => console.log("error, ", error),
});

return {
questionnaires: data || [],
isFetching: isLoading,
Expand Down Expand Up @@ -76,3 +76,28 @@ export const useFetchQuestionnaireById = (id: number | string) => {
fetchError: error,
};
};

export const useCreateQuestionnaireMutation = (
onSuccess?: (res: Questionnaire) => void,
onError?: (err: AxiosError) => void
) => {
const queryClient = useQueryClient();
const { isLoading, mutate, mutateAsync, error } = useMutation(
createQuestionnaire,
{
onSuccess: (res) => {
onSuccess && onSuccess(res);
queryClient.invalidateQueries([]);
},
onError: (err: AxiosError) => {
onError && onError(err);
},
}
);
return {
mutate,
mutateAsync,
isLoading,
error,
};
};
8 changes: 4 additions & 4 deletions client/src/mocks/stub-new-work/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import applications from "./applications";
import archetypes from "./archetypes";

export default [
...questionnaires,
...assessments,
...applications,
...archetypes,
// ...questionnaires,
// ...assessments,
// ...applications,
// ...archetypes,
] as RestHandler[];

0 comments on commit 73f5571

Please sign in to comment.