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

✨ Wire up questionnaire upload to hub api #1341

Merged
merged 2 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[];