Skip to content

Feature 1374/fix url validations on feedback request flow #1406

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

Merged
Merged
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
244 changes: 128 additions & 116 deletions web-ui/src/pages/FeedbackRequestPage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useContext, useCallback, useEffect, useState} from "react";
import React, {useContext, useCallback, useEffect, useState, useRef} from "react";
import { makeStyles } from "@material-ui/core/styles";
import Stepper from "@material-ui/core/Stepper";
import Step from "@material-ui/core/Step";
Expand Down Expand Up @@ -78,23 +78,14 @@ const FeedbackRequestPage = () => {
const currentUserId = memberProfile?.id;
const location = useLocation();
const history = useHistory();
const [query, setQuery] = useState({});
const stepQuery = query.step?.toString();
const templateQuery = query.template?.toString();
const fromQuery = query.from ? query.from : [];
const sendQuery = query.send?.toString();
const dueQuery = query.due?.toString();
const sendDate = query.send?.toString();
const forQuery = query.for?.toString();
const requestee = selectProfile(state, forQuery);
const memberIds = selectCurrentMemberIds(state);
const csrf = selectCsrfToken(state);
const [query, setQuery] = useState({});
const queryLoaded = useRef(false);
const [readyToProceed, setReadyToProceed] = useState(false);
const [templateIsValid, setTemplateIsValid] = useState();

useEffect(() => {
setQuery(queryString.parse(location?.search));
}, [location.search]);
const [requestee, setRequestee] = useState({});
const [memberIds, setMemberIds] = useState([]);
const [activeStep, setActiveStep] = useState(1);

const handleQueryChange = useCallback((key, value) => {
let newQuery = {
Expand All @@ -105,51 +96,18 @@ const FeedbackRequestPage = () => {
}, [history, location, query]);

const getStep = useCallback(() => {
if (!stepQuery || stepQuery < 1 || !/^\d+$/.test(stepQuery))
if (!query.step || query.step < 1 || !/^\d+$/.test(query.step))
return 1;
else return parseInt(stepQuery);
}, [stepQuery]);

const activeStep = getStep();
else return parseInt(query.step);
}, [query.step]);

const hasFor = useCallback(() => {
return !!forQuery;
}, [forQuery])

useEffect(() => {
async function isTemplateValid() {
if (!templateQuery || !csrf) {
return false;
}
let res = await getFeedbackTemplate(templateQuery, csrf);
let templateResponse =
res.payload &&
res.payload.data &&
res.payload.status === 200 &&
!res.error
? res.payload.data
: null
if (templateResponse === null) {
window.snackDispatch({
type: UPDATE_TOAST,
payload: {
severity: "error",
toast: "The ID for the template you selected does not exist.",
},
});
return false;
}
else {
return true;
}
}

isTemplateValid().then((isValid) => {
setTemplateIsValid(isValid);
});
}, [csrf, templateQuery]);
if (!memberIds.length) return true;
return !!query.for && memberIds.includes(query.for);
}, [query.for, memberIds]);

const hasFrom = useCallback(() => {
if (!memberIds.length) return true;
let from = query.from;
if (from) {
from = Array.isArray(from) ? from : [from];
Expand Down Expand Up @@ -182,38 +140,64 @@ const FeedbackRequestPage = () => {
}, []);

const hasSend = useCallback(() => {
const isValidPair = dueQuery ? dueQuery >= sendQuery : true;
return (sendQuery && isValidDate(sendQuery) && isValidPair)
}, [sendQuery, isValidDate, dueQuery]);
const isValidPair = query.due ? query.due >= query.send : true;
return (query.send && isValidDate(query.send) && isValidPair)
}, [query.send, isValidDate, query.due]);

const canProceed = useCallback(() => {
if (query && Object.keys(query).length > 0) {
switch (activeStep) {
case 1:
return hasFor() && templateIsValid
case 2:
return hasFor() && templateIsValid && hasFrom();
case 3:
const dueQueryValid = dueQuery ? isValidDate(dueQuery) : true;
return hasFor() && templateIsValid && hasFrom() && hasSend() && dueQueryValid;
default:
return false;
if (activeStep === 1) {
return hasFor() && templateIsValid;
} else if (activeStep === 2) {
return hasFor() && templateIsValid && hasFrom();
} else if (activeStep === 3) {
const dueQueryValid = query.due ? isValidDate(query.due) : true;
return hasFor() && templateIsValid && hasFrom() && hasSend() && dueQueryValid;
} else {
return false;
}
}
return false;
}, [activeStep, hasFor, hasFrom, hasSend, dueQuery, isValidDate, query, templateIsValid]);
}, [hasFor, hasFrom, hasSend, isValidDate, query, templateIsValid, activeStep]);

const sendFeedbackRequest = async (feedbackRequest) => {
if (csrf) {
let res = await createFeedbackRequest(feedbackRequest, csrf);
let data =
res.payload && res.payload.data && !res.error
? res.payload.data
: null;
if (data) {
// If the request was successful, set created ad-hoc templates to inactive
await softDeleteAdHoc(currentUserId);
const newLocation = {
pathname: "/feedback/request/confirmation",
search: queryString.stringify(query),
}
history.push(newLocation)
} else if (res.error || data === null) {
dispatch({
type: UPDATE_TOAST,
payload: {
severity: "error",
toast: "An error has occurred while submitting your request.",
},
});
}
}
}

const handleSubmit = () => {
const from = fromQuery ? (Array.isArray(fromQuery) ? fromQuery : [fromQuery]) : [];
const from = query.from ? (Array.isArray(query.from) ? query.from : [query.from]) : [];
for (const recipient of from) {
const feedbackRequest = {
id: null,
creatorId: currentUserId,
requesteeId: forQuery,
requesteeId: query.for,
recipientId: recipient,
templateId: templateQuery,
sendDate: sendDate,
dueDate: dueQuery,
templateId: query.template,
sendDate: query.send,
dueDate: query.due,
status: "pending",
submitDate: null
};
Expand All @@ -230,66 +214,94 @@ const FeedbackRequestPage = () => {
query.step = `${activeStep + 1}`;
history.push({...location, search: queryString.stringify(query)});

}, [canProceed, activeStep, steps.length, query, location, history]); // eslint-disable-line react-hooks/exhaustive-deps
}, [canProceed, steps.length, query, location, history]); // eslint-disable-line react-hooks/exhaustive-deps

const onBackClick = useCallback(() => {
if (activeStep === 1) return;
query.step = `${activeStep - 1}`;
history.push({ ...location, search: queryString.stringify(query) });
}, [activeStep, query, location, history]);
}, [query, location, history, activeStep]);

const softDeleteAdHoc = useCallback(async (creatorId) => {
if (csrf) {
await softDeleteAdHocTemplates(creatorId, csrf);
}
}, [csrf]);

const sendFeedbackRequest = async (feedbackRequest) => {
if (csrf) {
let res = await createFeedbackRequest(feedbackRequest, csrf);
let data =
res.payload && res.payload.data && !res.error
const urlIsValid = useCallback(() => {
if (query) {
if (activeStep === 1) {
return hasFor();
} else if (activeStep === 2) {
return hasFor() && templateIsValid;
} else if (activeStep === 3) {
return hasFor() && templateIsValid && hasFrom();
} else {
return false;
}
}
return false;
}, [hasFor, hasFrom, query, templateIsValid, activeStep]);

useEffect(() => {
setActiveStep(getStep());
}, [query.step, getStep]);

useEffect(() => {
const members = selectCurrentMemberIds(state);
if (members) {
setMemberIds(members);
}
}, [state]);

useEffect(() => {
const params = queryString.parse(location?.search);
setQuery(params);
}, [location.search]);

useEffect(() => {
async function isTemplateValid() {
if (!query.template || !csrf) {
return false;
}
let res = await getFeedbackTemplate(query.template, csrf);
let templateResponse =
res.payload &&
res.payload.data &&
res.payload.status === 200 &&
!res.error
? res.payload.data
: null;
if (data) {
// If the request was successful, set created ad-hoc templates to inactive
await softDeleteAdHoc(currentUserId);
const newLocation = {
pathname: "/feedback/request/confirmation",
search: queryString.stringify(query),
}
history.push(newLocation)
} else if(res.error || data === null) {
dispatch({
: null
if (templateResponse === null) {
window.snackDispatch({
type: UPDATE_TOAST,
payload: {
severity: "error",
toast: "An error has occurred while submitting your request.",
toast: "The ID for the template you selected does not exist.",
},
});
return false;
}
else {
return true;
}
}
}

const urlIsValid = useCallback(() => {
if (query && Object.keys(query).length > 0) {
switch (activeStep) {
case 1:
return hasFor();
case 2:
return hasFor() && templateIsValid
case 3:
return hasFor() && templateIsValid && hasFrom();
case 4:
return hasFor() && templateIsValid && hasFrom() && hasSend();
default:
return false;
}
if (queryLoaded.current && csrf) {
isTemplateValid().then((isValid) => {
setTemplateIsValid(isValid);
});
} else {
queryLoaded.current = true;
}
return true;
}, [activeStep, hasFor, hasFrom, hasSend, query, templateIsValid]);
}, [csrf, query, queryLoaded]);

useEffect(() => {
if (!queryLoaded.current || templateIsValid === undefined) return;

if (query.for) {
setRequestee(selectProfile(state, query.for));
}
if (!urlIsValid()) {
dispatch({
type: UPDATE_TOAST,
Expand All @@ -302,11 +314,11 @@ const FeedbackRequestPage = () => {
history.push("/checkins");
});
}
}, [history, urlIsValid, dispatch, softDeleteAdHoc, currentUserId]);
}, [history, state, query, currentUserId, dispatch, softDeleteAdHoc, urlIsValid, templateIsValid]);

useEffect(()=> {
useEffect(() => {
setReadyToProceed(canProceed());
}, [canProceed])
}, [canProceed]);

return (
<div className="feedback-request-page">
Expand Down Expand Up @@ -337,9 +349,9 @@ const FeedbackRequestPage = () => {
</Stepper>
</div>
<div className="current-step-content">
{activeStep === 1 && <FeedbackTemplateSelector changeQuery={(key, value) => handleQueryChange(key, value)} query={templateQuery} />}
{activeStep === 2 && <FeedbackRecipientSelector changeQuery={(key, value) => handleQueryChange(key, value)} fromQuery={Array.isArray(fromQuery) ? fromQuery : [fromQuery]} />}
{activeStep === 3 && <SelectDate changeQuery={(key, value) => handleQueryChange(key, value)} sendDateQuery={sendQuery} dueDateQuery={dueQuery}/>}
{activeStep === 1 && <FeedbackTemplateSelector changeQuery={(key, value) => handleQueryChange(key, value)} query={query.template} />}
{activeStep === 2 && <FeedbackRecipientSelector changeQuery={(key, value) => handleQueryChange(key, value)} fromQuery={query.from ? (Array.isArray(query.from) ? query.from : [query.from]) : []} />}
{activeStep === 3 && <SelectDate changeQuery={(key, value) => handleQueryChange(key, value)} sendDateQuery={query.send} dueDateQuery={query.due}/>}
</div>
</div>
);
Expand Down