From 998ce2d53f872cca4424fd572704f8bd4ece369d Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 11:58:21 -0700 Subject: [PATCH 1/6] DESENG-642: Survey Data Router Features --- CHANGELOG.MD | 8 + .../met_api/resources/email_verification.py | 12 +- .../services/email_verification_service.py | 6 +- met-web/src/components/Form/formio.scss | 2 +- .../src/components/common/Input/Button.tsx | 2 + .../src/components/common/Input/TextInput.tsx | 2 +- .../components/common/Typography/Headers.tsx | 2 +- .../engagement/new/view/EngagementHero.tsx | 1 + .../new/view/EngagementSurveyBlock.tsx | 2 - .../components/engagement/view/EmailPanel.tsx | 6 +- .../survey/building/SurveyLoader.tsx | 28 ++ .../src/components/survey/building/index.tsx | 434 ++++++++++-------- .../components/survey/edit/ActionContext.tsx | 207 --------- .../src/components/survey/edit/EditForm.tsx | 102 +++- .../components/survey/edit/FormWrapped.tsx | 41 +- .../survey/edit/InvalidTokenModal.tsx | 48 -- met-web/src/components/survey/edit/index.tsx | 23 +- .../survey/report/ReportSettingsContext.tsx | 187 -------- .../components/survey/report/SearchBar.tsx | 59 +-- .../components/survey/report/SettingsForm.tsx | 302 +++++++----- .../survey/report/SettingsTable.tsx | 44 +- .../src/components/survey/report/index.tsx | 7 +- .../survey/submit/ActionContext.tsx | 226 --------- .../survey/submit/EngagementLink.tsx | 22 +- .../survey/submit/InvalidTokenModal.tsx | 58 ++- .../survey/submit/PreviewBanner.tsx | 47 +- .../components/survey/submit/SurveyBanner.tsx | 43 +- .../components/survey/submit/SurveyForm.tsx | 122 ++++- .../survey/submit/SurveySubmitWrapped.tsx | 59 --- .../src/components/survey/submit/index.tsx | 67 ++- met-web/src/components/survey/types.ts | 5 - met-web/src/models/emailVerification.ts | 1 + met-web/src/routes/AuthenticatedRoutes.tsx | 9 +- met-web/src/routes/UnauthenticatedRoutes.tsx | 12 +- 34 files changed, 931 insertions(+), 1265 deletions(-) create mode 100644 met-web/src/components/survey/building/SurveyLoader.tsx delete mode 100644 met-web/src/components/survey/edit/ActionContext.tsx delete mode 100644 met-web/src/components/survey/edit/InvalidTokenModal.tsx delete mode 100644 met-web/src/components/survey/report/ReportSettingsContext.tsx delete mode 100644 met-web/src/components/survey/submit/ActionContext.tsx delete mode 100644 met-web/src/components/survey/submit/SurveySubmitWrapped.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index f6e065db8..e88becfb2 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,11 @@ +## July 9, 2024 + +- **Task** Add data router features to survey pages [DESENG-642](https://citz-gdx.atlassian.net/browse/DESENG-642) + - Remove "ActionContext" files and fetches to survey service + - Replace with SurveyLoader on the survey parent route that creates survey data promises + - Restructure survey pages to use \ / \ for data loading + - Add useBlocker() call to the survey authoring and survey submission pages to prevent users from navigating away from the page while in the middle of creating or submitting a survey + ## June 27, 2024 - **Bugfix** Fix tenant landing page [🎟️ DESENG-635](https://citz-gdx.atlassian.net/browse/DESENG-635) diff --git a/met-api/src/met_api/resources/email_verification.py b/met-api/src/met_api/resources/email_verification.py index d01ab40a2..662f11eb5 100644 --- a/met-api/src/met_api/resources/email_verification.py +++ b/met-api/src/met_api/resources/email_verification.py @@ -38,7 +38,7 @@ class EmailVerification(Resource): # @TRACER.trace() @cross_origin(origins=allowedorigins()) def get(token): - """Fetch a email verification matching the provided token.""" + """Fetch an email verification matching the provided token.""" try: email_verification = EmailVerificationService().get_active(token) if email_verification: @@ -54,7 +54,7 @@ def get(token): # @TRACER.trace() @cross_origin(origins=allowedorigins()) def put(token): - """Fetch a email verification matching the provided token.""" + """Update an email verification matching the provided token.""" try: email_verification = EmailVerificationService().verify(token, None, None, None) if email_verification: @@ -69,7 +69,7 @@ def put(token): @cors_preflight('POST, OPTIONS') @API.route('/') class EmailVerifications(Resource): - """Resource for managing email verifications.""" + """Resource for managing email verifications for survey submissions.""" @staticmethod # @TRACER.trace() @@ -80,6 +80,8 @@ def post(): requestjson = request.get_json() email_verification = EmailVerificationSchema().load(requestjson) created_email_verification = EmailVerificationService().create(email_verification) + # don't return the verification token when creating a new email verification + created_email_verification.pop('verification_token') return created_email_verification, HTTPStatus.OK except KeyError as err: return str(err), HTTPStatus.INTERNAL_SERVER_ERROR @@ -90,7 +92,7 @@ def post(): @cors_preflight('POST, OPTIONS') @API.route('//subscribe') class SubscribeEmailVerifications(Resource): - """Resource for managing email verifications.""" + """Resource for managing email verifications for subscriptions.""" @staticmethod # @TRACER.trace() @@ -101,6 +103,8 @@ def post(subscription_type): requestjson = request.get_json() email_verification = EmailVerificationSchema().load(requestjson) created_email_verification = EmailVerificationService().create(email_verification, subscription_type) + # don't return the verification token when creating a new email verification + created_email_verification.pop('verification_token') return created_email_verification, HTTPStatus.OK except KeyError as err: return str(err), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/met-api/src/met_api/services/email_verification_service.py b/met-api/src/met_api/services/email_verification_service.py index 9f3cb7956..7b2d89b3b 100644 --- a/met-api/src/met_api/services/email_verification_service.py +++ b/met-api/src/met_api/services/email_verification_service.py @@ -63,12 +63,12 @@ def create(cls, email_verification: EmailVerificationSchema, email_verification['created_by'] = email_verification.get( 'participant_id') verification_token = uuid.uuid4() - EmailVerification.create({**email_verification, 'verification_token': verification_token}, session) + email_verification['verification_token'] = verification_token + EmailVerification.create(email_verification, session) # TODO: remove this once email logic is brought over from submission service to here if email_verification.get('type', None) != EmailVerificationType.RejectedComment: - cls._send_verification_email( - {**email_verification, 'verification_token': verification_token}, subscription_type) + cls._send_verification_email(email_verification, subscription_type) return email_verification diff --git a/met-web/src/components/Form/formio.scss b/met-web/src/components/Form/formio.scss index 0e6a4f172..811d22c1b 100644 --- a/met-web/src/components/Form/formio.scss +++ b/met-web/src/components/Form/formio.scss @@ -191,7 +191,7 @@ i.fa.fa-question-circle.text-muted { .nav-link { text-decoration: none; } -.active { +.active:not(.MuiLink-root, .MuiPaper-root) { background-color: #fff; } .active-tab { diff --git a/met-web/src/components/common/Input/Button.tsx b/met-web/src/components/common/Input/Button.tsx index bf940acba..b10dc772a 100644 --- a/met-web/src/components/common/Input/Button.tsx +++ b/met-web/src/components/common/Input/Button.tsx @@ -11,6 +11,7 @@ import { isDarkColor } from 'utils'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { styled } from '@mui/system'; +import { RouterLinkRenderer } from '../Navigation/Link'; const buttonStyles = { borderRadius: '16px', @@ -251,6 +252,7 @@ export const Button = ({ }: ButtonProps & { variant?: 'primary' | 'secondary' | 'tertiary'; }) => { + props.LinkComponent = props.LinkComponent || RouterLinkRenderer; switch (variant) { case 'primary': return ; diff --git a/met-web/src/components/common/Input/TextInput.tsx b/met-web/src/components/common/Input/TextInput.tsx index e0a0b62e6..0759c54a9 100644 --- a/met-web/src/components/common/Input/TextInput.tsx +++ b/met-web/src/components/common/Input/TextInput.tsx @@ -122,7 +122,7 @@ export type TextFieldProps = { counter?: boolean; maxLength?: number; clearable?: boolean; - onChange: (value: string, name?: string) => void; + onChange?: (value: string, name?: string) => void; } & Omit & Omit; diff --git a/met-web/src/components/common/Typography/Headers.tsx b/met-web/src/components/common/Typography/Headers.tsx index dc7170e04..4d8e328b5 100644 --- a/met-web/src/components/common/Typography/Headers.tsx +++ b/met-web/src/components/common/Typography/Headers.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Typography, TypographyProps } from '@mui/material'; +import { Palette } from 'styles/Theme'; const fontWeight = (weight?: string | number) => { switch (weight) { @@ -35,7 +36,6 @@ export const Header1 = ({ marginBottom: '2rem', marginTop: '1.5rem', fontWeight: fontWeight(weight), - color: '#292929', ...props.sx, }} > diff --git a/met-web/src/components/engagement/new/view/EngagementHero.tsx b/met-web/src/components/engagement/new/view/EngagementHero.tsx index 95a4a9759..25a95cc66 100644 --- a/met-web/src/components/engagement/new/view/EngagementHero.tsx +++ b/met-web/src/components/engagement/new/view/EngagementHero.tsx @@ -115,6 +115,7 @@ export const EngagementHero = () => { diff --git a/met-web/src/components/engagement/view/EmailPanel.tsx b/met-web/src/components/engagement/view/EmailPanel.tsx index 797f06333..9325d4d71 100644 --- a/met-web/src/components/engagement/view/EmailPanel.tsx +++ b/met-web/src/components/engagement/view/EmailPanel.tsx @@ -8,9 +8,13 @@ import { INTERNAL_EMAIL_DOMAIN } from 'constants/emailVerification'; import { Editor } from 'react-draft-wysiwyg'; import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; import { Button, CustomTextField } from 'components/common/Input'; +import { useAsyncValue } from 'react-router-dom'; +import { Engagement } from 'models/engagement'; const EmailPanel = ({ email, checkEmail, handleClose, updateEmail, isSaving, isInternal }: EmailPanelProps) => { + const loadedEngagement = useAsyncValue() as [Engagement] | undefined; const { savedEngagement } = useContext(ActionContext); + const engagement = loadedEngagement ? loadedEngagement[0] : savedEngagement; const [checked, setChecked] = useState(false); const [emailFormError, setEmailFormError] = useState({ terms: false, @@ -79,7 +83,7 @@ const EmailPanel = ({ email, checkEmail, handleClose, updateEmail, isSaving, isI diff --git a/met-web/src/components/survey/building/SurveyLoader.tsx b/met-web/src/components/survey/building/SurveyLoader.tsx new file mode 100644 index 000000000..fad3a662a --- /dev/null +++ b/met-web/src/components/survey/building/SurveyLoader.tsx @@ -0,0 +1,28 @@ +import { EmailVerification } from 'models/emailVerification'; +import { Params, defer } from 'react-router-dom'; +import { getEmailVerification } from 'services/emailVerificationService'; +import { getEngagement } from 'services/engagementService'; +import { getSlugByEngagementId } from 'services/engagementSlugService'; +import { getSubmission, getSubmissionByToken } from 'services/submissionService'; +import { getSurvey } from 'services/surveyService'; +import { fetchSurveyReportSettings } from 'services/surveyService/reportSettingsService'; + +export const SurveyLoader = async ({ params }: { params: Params }) => { + const { surveyId, token, language } = params; + if (isNaN(Number(surveyId)) && !token) throw new Error('Invalid survey ID'); + const verification = getEmailVerification(token ?? '').catch(() => null); + const survey = surveyId + ? getSurvey(Number(surveyId)) + : verification.then((response) => { + if (!response) throw new Error('Invalid token'); + return getSurvey(response.survey_id); + }); + const submission = verification.then((response) => getSubmissionByToken(response?.verification_token ?? '')); + const reportSettings = survey.then((response) => fetchSurveyReportSettings(response.id.toString())); + const engagement = survey.then((response) => { + if (!response.engagement_id) return null; + return getEngagement(response.engagement_id); + }); + const slug = engagement.then((response) => response && getSlugByEngagementId(response.id)); + return defer({ engagement, language, reportSettings, slug, submission, survey, surveyId, token, verification }); +}; diff --git a/met-web/src/components/survey/building/index.tsx b/met-web/src/components/survey/building/index.tsx index 07eb795c8..00ccf0a45 100644 --- a/met-web/src/components/survey/building/index.tsx +++ b/met-web/src/components/survey/building/index.tsx @@ -1,4 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, Suspense } from 'react'; +import { useBlocker, useNavigate, useAsyncValue, useRouteLoaderData, Await, useRevalidator } from 'react-router-dom'; +import { Survey } from 'models/survey'; +import FormBuilderSkeleton from './FormBuilderSkeleton'; +import { Engagement } from 'models/engagement'; import { Grid, Stack, @@ -9,71 +13,96 @@ import { FormGroup, FormControlLabel, Tooltip, + Avatar, } from '@mui/material'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCircleQuestion } from '@fortawesome/pro-solid-svg-icons/faCircleQuestion'; -import { faPencilSlash } from '@fortawesome/pro-regular-svg-icons/faPencilSlash'; -import { faPen } from '@fortawesome/pro-regular-svg-icons/faPen'; -import { useNavigate, useParams } from 'react-router-dom'; import FormBuilder from 'components/Form/FormBuilder'; -import { SurveyParams } from '../types'; -import { getSurvey, putSurvey } from 'services/surveyService'; -import { Survey } from 'models/survey'; +import { putSurvey } from 'services/surveyService'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; -import { MetHeader3, MetPageGridContainer, PrimaryButtonOld, SecondaryButtonOld } from 'components/common'; -import FormBuilderSkeleton from './FormBuilderSkeleton'; +import { MetHeader3, MetPageGridContainer, MetTooltip } from 'components/common'; import { FormBuilderData } from 'components/Form/types'; import { EngagementStatus } from 'constants/engagementStatus'; -import { getEngagement } from 'services/engagementService'; -import { Engagement } from 'models/engagement'; import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; -import { Palette } from 'styles/Theme'; +import { Palette, colors } from 'styles/Theme'; import { PermissionsGate } from 'components/permissionsGate'; import { USER_ROLES } from 'services/userService/constants'; import axios from 'axios'; import { AutoSaveSnackBar } from './AutoSaveSnackBar'; -import { debounce } from 'lodash'; +import { debounce, set } from 'lodash'; +import { Button } from 'components/common/Input'; +import { Controller, useForm } from 'react-hook-form'; +import { + faCircleQuestion, + faPen, + faPenSlash, + faCloudArrowUp, + faCloudCheck, + faCloudXmark, +} from '@fortawesome/pro-regular-svg-icons'; +import { Else, If, Then } from 'react-if'; interface SurveyForm { id: string; - form_json: unknown; + form_json: FormBuilderData; name: string; is_hidden?: boolean; is_template?: boolean; } -const SurveyFormBuilder = () => { +export const FormBuilderPage = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const { surveyId } = useParams(); + const revalidator = useRevalidator(); + + const [survey, engagement] = useAsyncValue() as [Survey, Engagement]; + const [formDefinition, setFormDefinition] = useState(survey.form_json); + const [isFormDirty, setIsFormDirty] = useState(false); + const [hasChanged, setHasChanged] = useState(false); + const [saveError, setSaveError] = useState(false); + + const { + control, + formState: { isSubmitting, isSubmitted, isDirty }, + handleSubmit, + reset, + watch, + } = useForm>({ + defaultValues: { + id: survey.id.toString(), + name: survey.name, + is_hidden: survey.is_hidden, + is_template: survey.is_template, + }, + }); + + useEffect(() => { + const subscription = watch((value, { name, type }) => { + // Auto save form when any field changes + if (type === 'change') { + setHasChanged(true); + debounceAutoSaveForm(formDefinition); + } + }); + return () => subscription.unsubscribe(); + }, [watch]); - const [savedSurvey, setSavedSurvey] = useState(null); - const [formData, setFormData] = useState<(unknown & { components: unknown[] }) | null>(null); + const name = watch('name'); + const hasUnsavedWork = (isDirty || isFormDirty) && !isSubmitting; - const [loading, setLoading] = useState(true); - const [isNameFocused, setIsNamedFocused] = useState(false); - const [name, setName] = useState(savedSurvey ? savedSurvey.name : ''); - const [isSaving, setIsSaving] = useState(false); - const [savedEngagement, setSavedEngagement] = useState(null); + const isMultiPage = formDefinition?.display === 'wizard'; - const [formDefinition, setFormDefinition] = useState({ display: 'form', components: [] }); - const isMultiPage = formDefinition.display === 'wizard'; - const hasEngagement = Boolean(savedSurvey?.engagement_id); - const isEngagementDraft = savedEngagement?.status_id === EngagementStatus.Draft; + const hasEngagement = Boolean(survey?.engagement_id); + const isEngagementDraft = engagement?.status_id === EngagementStatus.Draft; const hasPublishedEngagement = hasEngagement && !isEngagementDraft; - const [isHiddenSurvey, setIsHiddenSurvey] = useState(savedSurvey ? savedSurvey.is_hidden : false); - const [isTemplateSurvey, setIsTemplateSurvey] = useState(savedSurvey ? savedSurvey.is_template : false); + const [isEditingName, setIsEditingName] = useState(false); const [autoSaveNotificationOpen, setAutoSaveNotificationOpen] = useState(false); - const AUTO_SAVE_INTERVAL = 5000; - useEffect(() => { - loadSurvey(); - }, []); + const AUTO_SAVE_INTERVAL = 5 * 1000; useEffect(() => { - if (savedEngagement && hasPublishedEngagement) { + if (hasPublishedEngagement) { dispatch( openNotification({ severity: 'warning', @@ -81,131 +110,65 @@ const SurveyFormBuilder = () => { }), ); } - }, [savedEngagement]); + }, [hasPublishedEngagement, dispatch]); - const loadSurvey = async () => { - if (isNaN(Number(surveyId))) { - navigate('/surveys'); - dispatch( - openNotification({ - severity: 'error', - text: 'The survey id passed was erroneous', - }), - ); - return; - } + const debounceAutoSaveForm = useRef(debounce((data) => autoSaveForm(data), AUTO_SAVE_INTERVAL)).current; + const autoSaveForm = async (formDef: FormBuilderData) => { try { - const loadedSurvey = await getSurvey(Number(surveyId)); - setSavedSurvey(loadedSurvey); - setFormDefinition(loadedSurvey?.form_json || { display: 'form', components: [] }); - setName(loadedSurvey.name); - setIsHiddenSurvey(loadedSurvey.is_hidden); - setIsTemplateSurvey(loadedSurvey.is_template); + await handleSubmit(async (data: Omit) => { + const { form_json, ...result } = await putSurvey({ + ...data, + form_json: formDef, + }); + reset(result as Omit); + setAutoSaveNotificationOpen(true); + setIsFormDirty(false); + })(); + setSaveError(false); } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while loading saved survey', - }), - ); - navigate('/surveys'); + setSaveError(true); } }; - useEffect(() => { - if (savedSurvey) { - loadEngagement(); - } - }, [savedSurvey]); + const hasMounted = useRef(false); - const loadEngagement = async () => { - if (!savedSurvey?.engagement_id) { - setLoading(false); + const onEditorChange = (form: FormBuilderData) => { + if (!hasMounted.current) { + // Skip the first call to onEditorChange - it's the initial form load; + // we don't want to send it back to the server right away + hasMounted.current = true; return; } + setIsFormDirty(true); + setFormDefinition(form); - try { - const loadedEngagement = await getEngagement(Number(savedSurvey.engagement_id)); - setSavedEngagement(loadedEngagement); - setLoading(false); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while loading saved engagement data', - }), - ); - navigate('/survey/listing'); - } - }; - - const debounceAutoSaveForm = useRef( - debounce((newChanges: SurveyForm) => { - autoSaveForm(newChanges); - }, AUTO_SAVE_INTERVAL), - ).current; - - const doDebounceSaveForm = (form: FormBuilderData) => { - debounceAutoSaveForm({ - id: String(surveyId), - form_json: form, - name: name, - }); - }; - - const handleFormChange = (form: FormBuilderData) => { if (!form.components) { return; } - setFormData(form); - doDebounceSaveForm(form); + setHasChanged(true); + debounceAutoSaveForm(form); }; - const autoSaveForm = async (newForm: SurveyForm) => { + const formSubmitHandler = async (data: Omit) => { try { - await putSurvey(newForm); - setAutoSaveNotificationOpen(true); - } catch (error) { - return; - } - }; - - const doSaveForm = async () => { - await putSurvey({ - id: String(surveyId), - form_json: formData, - name: name, - is_hidden: isHiddenSurvey, - is_template: isTemplateSurvey, - }); - }; - - const handleSaveForm = async () => { - if (!savedSurvey) { - dispatch( - openNotification({ - severity: 'error', - text: 'Unable to build survey, please reload', - }), - ); - return; - } - - try { - setIsSaving(true); - await doSaveForm(); - dispatch( - openNotification({ - severity: 'success', - text: savedSurvey.engagement?.id - ? `Survey was successfully added to engagement` - : 'The survey was successfully built', - }), - ); - - navigate(`/surveys/${savedSurvey.id}/report`); + if (hasUnsavedWork) { + await putSurvey({ + ...data, + id: survey.id.toString(), + form_json: formDefinition, + }); + dispatch( + openNotification({ + severity: 'success', + text: survey.engagement?.id + ? `Survey was successfully added to engagement` + : 'The survey was successfully built', + }), + ); + } + if (hasChanged) revalidator.revalidate(); + navigate(`/surveys/${survey.id}/report`); } catch (error) { - setIsSaving(false); if (axios.isAxiosError(error)) { dispatch( openNotification({ @@ -224,9 +187,31 @@ const SurveyFormBuilder = () => { } }; - if (loading) { - return ; - } + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => hasUnsavedWork && nextLocation.pathname !== currentLocation.pathname, + ); + + useEffect(() => { + if (blocker.state === 'blocked') { + dispatch( + openNotificationModal({ + open: true, + data: { + style: 'warning', + header: 'Unsaved Changes', + subHeader: + 'If you leave this page, your changes will not be saved. Are you sure you want to leave this page?', + subText: [], + confirmButtonText: 'Leave', + cancelButtonText: 'Stay', + handleConfirm: blocker.proceed, + handleClose: blocker.reset, + }, + type: 'confirm', + }), + ); + } + }, [blocker, dispatch]); return ( { > - {!isNameFocused ? ( - <> + + + ( + { + setIsEditingName(false); + }} + /> + )} + /> + { + setIsEditingName(!isEditingName); + }} + color="inherit" + > + + + + { - setIsNamedFocused(true); + setIsEditingName(true); }} > {name} @@ -253,36 +261,25 @@ const SurveyFormBuilder = () => { { - setIsNamedFocused(!isNameFocused); + setIsEditingName(!isEditingName); }} color="inherit" > - - ) : ( - <> - setName(event.target.value)} /> - { - setIsNamedFocused(!isNameFocused); - }} - color="inherit" - > - - - - )} + + + - + { + onChange={() => { dispatch( openNotificationModal({ open: true, @@ -305,7 +302,7 @@ const SurveyFormBuilder = () => { }, ], handleConfirm: () => { - setFormDefinition({ + onEditorChange({ display: isMultiPage ? 'form' : 'wizard', components: [], }); @@ -322,7 +319,7 @@ const SurveyFormBuilder = () => { - + @@ -330,16 +327,12 @@ const SurveyFormBuilder = () => { - { - if (e.target.checked) { - setIsTemplateSurvey(true); - return; - } - setIsTemplateSurvey(false); - }} + ( + + )} /> } @@ -371,16 +364,10 @@ const SurveyFormBuilder = () => { - { - if (e.target.checked) { - setIsHiddenSurvey(true); - return; - } - setIsHiddenSurvey(false); - }} + } /> } @@ -410,10 +397,12 @@ const SurveyFormBuilder = () => { - - {'Report Settings'} - - navigate('/surveys')}>Cancel + + { ); }; +const SaveStatusIndicator = ({ hasUnsavedWork, saveError }: { hasUnsavedWork: boolean; saveError: boolean }) => { + const saveStatusData = () => { + if (saveError) + return { + icon: faCloudXmark, + color: colors.notification.error.icon, + tint: colors.notification.error.tint, + text: 'Error saving', + }; + if (hasUnsavedWork) + return { + icon: faCloudArrowUp, + color: colors.notification.warning.icon, + tint: colors.notification.warning.tint, + text: 'Saving...', + }; + return { + icon: faCloudCheck, + color: colors.notification.success.icon, + tint: colors.notification.success.tint, + text: 'Saved', + }; + }; + const { icon, color, tint, text } = saveStatusData(); + return ( + + + + + + ); +}; + +const SurveyFormBuilder = () => { + const { survey: surveyPromise, engagement: engagementPromise } = useRouteLoaderData('survey') as { + survey: Promise; + engagement: Promise; + }; + + const surveyDataPromise = Promise.all([surveyPromise, engagementPromise]); + + return ( + }> + + + + + ); +}; + export default SurveyFormBuilder; diff --git a/met-web/src/components/survey/edit/ActionContext.tsx b/met-web/src/components/survey/edit/ActionContext.tsx deleted file mode 100644 index 3222eee0f..000000000 --- a/met-web/src/components/survey/edit/ActionContext.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { createContext, useState, useEffect, Dispatch, SetStateAction } from 'react'; -import { useAppDispatch } from 'hooks'; -import { useNavigate, useParams } from 'react-router-dom'; -import { openNotification } from 'services/notificationService/notificationSlice'; -import { getEmailVerification } from 'services/emailVerificationService'; -import { getEngagement } from 'services/engagementService'; -import { getSubmissionByToken, updateSubmission } from 'services/submissionService'; -import { Engagement } from 'models/engagement'; -import { PublicSubmission } from 'models/surveySubmission'; -import { getEngagementIdBySlug } from 'services/engagementSlugService'; -import { useAppTranslation } from 'hooks'; - -type EditSurveyParams = { - token: string; - engagementId?: string; - slug?: string; -}; - -interface EditSurveyContext { - token?: string; - isTokenValid: boolean; - handleSubmit: () => void; - isSubmitting: boolean; - savedEngagement: Engagement | null; - isLoading: boolean; - loadEngagement: null | (() => void); - submission: PublicSubmission | null; - setSubmission: Dispatch>; -} - -export const ActionContext = createContext({ - isTokenValid: true, - handleSubmit: () => { - return; - }, - setSubmission: () => { - return; - }, - isSubmitting: false, - savedEngagement: null, - isLoading: true, - loadEngagement: null, - submission: null, -}); - -export const ActionProvider = ({ children }: { children: JSX.Element }) => { - const { t: translate } = useAppTranslation(); - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const { engagementId: engagementIdParam, token, slug } = useParams(); - - const [engagementId, setEngagementId] = useState( - engagementIdParam ? Number(engagementIdParam) : null, - ); - const [isTokenValid, setTokenValid] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); - const [savedEngagement, setSavedEngagement] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [submission, setSubmission] = useState(null); - - const handleFetchEngagementIdBySlug = async () => { - if (!slug) { - return; - } - try { - const result = await getEngagementIdBySlug(slug); - setEngagementId(result.engagement_id); - } catch (error) { - navigate('/not-found'); - } - }; - - useEffect(() => { - handleFetchEngagementIdBySlug(); - }, [slug]); - - useEffect(() => { - fetchData(); - }, [engagementId]); - - const fetchData = async () => { - if (!engagementId && slug) { - return; - } - await loadEngagement(); - await verifyToken(); - await loadSubmission(); - setIsLoading(false); - }; - - const verifyToken = async () => { - if (!token) { - navigate(`/not-found`); - return; - } - - try { - const verification = await getEmailVerification(token); - if (!verification) { - throw new Error(translate('surveyEdit.surveyEditNotification.invalidToken')); - } - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveyEdit.surveyEditNotification.verificationError'), - }), - ); - setTokenValid(false); - } - }; - - const loadSubmission = async () => { - if (!token || !isTokenValid) { - return; - } - - try { - const loadedSubmission = await getSubmissionByToken(token); - if (loadedSubmission) { - if (loadedSubmission.engagement_id !== Number(engagementId)) { - throw translate('surveyEdit.surveyEditNotification.engagementError'); - } - setSubmission(loadedSubmission); - } - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveyEdit.surveyEditNotification.loadedSubmissionError'), - }), - ); - } - }; - - const loadEngagement = async () => { - if (isNaN(Number(engagementId))) { - navigate('/not-found'); - return; - } - - try { - const loadedEngagement = await getEngagement(Number(engagementId)); - setSavedEngagement(loadedEngagement); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveyEdit.surveyEditNotification.loadedEngagementError'), - }), - ); - } - }; - - const handleSubmit = async () => { - if (!token || !isTokenValid) { - return; - } - - try { - setIsSubmitting(true); - await updateSubmission(token, { - comments: submission?.comments ?? [], - }); - - dispatch( - openNotification({ - severity: 'success', - text: translate('surveyEdit.surveyEditNotification.success'), - }), - ); - navigate(`/engagements/${savedEngagement?.id}/view`, { - state: { - open: true, - }, - }); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveyEdit.surveyEditNotification.updateSurveyError'), - }), - ); - verifyToken(); - } finally { - setIsSubmitting(false); - } - }; - - return ( - - {children} - - ); -}; diff --git a/met-web/src/components/survey/edit/EditForm.tsx b/met-web/src/components/survey/edit/EditForm.tsx index 956bddba9..f88a5cb61 100644 --- a/met-web/src/components/survey/edit/EditForm.tsx +++ b/met-web/src/components/survey/edit/EditForm.tsx @@ -1,14 +1,89 @@ -import React, { useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import { cloneDeep } from 'lodash'; import { Grid, Stack, TextField } from '@mui/material'; -import { ActionContext } from './ActionContext'; -import { MetLabel, PrimaryButtonOld, SecondaryButtonOld } from 'components/common'; import { SurveyFormProps } from '../types'; -import { useAppTranslation } from 'hooks'; +import { useAppDispatch, useAppTranslation } from 'hooks'; +import { updateSubmission } from 'services/submissionService'; +import { openNotification } from 'services/notificationService/notificationSlice'; +import { useAsyncValue, useBlocker, useNavigate } from 'react-router-dom'; +import { Engagement } from 'models/engagement'; +import { SurveySubmission } from 'models/surveySubmission'; +import { EmailVerification } from 'models/emailVerification'; +import { Button } from 'components/common/Input'; +import { BodyText } from 'components/common/Typography'; +import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; export const EditForm = ({ handleClose }: SurveyFormProps) => { const { t: translate } = useAppTranslation(); - const { handleSubmit, isSubmitting, submission, setSubmission } = useContext(ActionContext); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [verification, , engagement, initialSubmission] = useAsyncValue() as [ + EmailVerification, + unknown, + Engagement, + SurveySubmission, + ]; + + const languagePath = `/${sessionStorage.getItem('languageId')}`; + + const [submission, setSubmission] = useState(initialSubmission); + + const isChanged = JSON.stringify(initialSubmission) !== JSON.stringify(submission); + + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => isChanged && nextLocation.pathname !== currentLocation.pathname, + ); + useEffect(() => { + if (blocker.state === 'blocked') { + dispatch( + openNotificationModal({ + open: true, + data: { + style: 'warning', + header: 'Unsaved Changes', + subHeader: + 'If you leave this page, your changes will not be saved. Are you sure you want to leave this page?', + subText: [], + confirmButtonText: 'Leave', + cancelButtonText: 'Stay', + handleConfirm: blocker.proceed, + handleClose: blocker.reset, + }, + type: 'confirm', + }), + ); + } + }, [blocker, dispatch]); + + const token = verification?.verification_token; + + const handleSubmit = async () => { + try { + if (!token) throw new Error('Token not found'); + await updateSubmission(token, { + comments: submission?.comments ?? [], + }); + + dispatch( + openNotification({ + severity: 'success', + text: translate('surveyEdit.surveyEditNotification.success'), + }), + ); + navigate(`/engagements/${engagement?.id}/view/${languagePath}`, { + state: { + open: true, + }, + }); + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: translate('surveyEdit.surveyEditNotification.updateSurveyError'), + }), + ); + } + }; const handleChange = (value: string, commentIndex: number) => { if (!submission) { @@ -16,7 +91,7 @@ export const EditForm = ({ handleClose }: SurveyFormProps) => { } const updatedSubmission = cloneDeep(submission); - updatedSubmission.comments[commentIndex].text = value; + if (updatedSubmission.comments?.[commentIndex]) updatedSubmission.comments[commentIndex].text = value; setSubmission(updatedSubmission); }; @@ -30,10 +105,10 @@ export const EditForm = ({ handleClose }: SurveyFormProps) => { mt={2} p={'0 2em 2em 2em'} > - {submission?.comments.map((comment, index) => { + {submission.comments?.map((comment, index) => { return ( - {comment.label} + {comment.label} { width="100%" justifyContent="flex-end" > - handleClose()}> + diff --git a/met-web/src/components/survey/edit/FormWrapped.tsx b/met-web/src/components/survey/edit/FormWrapped.tsx index 12eb08fc1..883f8340c 100644 --- a/met-web/src/components/survey/edit/FormWrapped.tsx +++ b/met-web/src/components/survey/edit/FormWrapped.tsx @@ -1,30 +1,33 @@ -import React, { useContext } from 'react'; +import React, { Suspense, useContext } from 'react'; import { Grid, Skeleton } from '@mui/material'; import { Banner } from 'components/banner/Banner'; import { EditForm } from './EditForm'; -import { ActionContext } from './ActionContext'; import { MetPaper } from 'components/common'; -import { InvalidTokenModal } from './InvalidTokenModal'; -import { useNavigate, useParams } from 'react-router'; +import { InvalidTokenModal } from '../submit/InvalidTokenModal'; import { When } from 'react-if'; import EngagementInfoSection from 'components/engagement/view/EngagementInfoSection'; +import { Await, useAsyncValue, useNavigate, useParams } from 'react-router-dom'; +import { EmailVerification } from 'models/emailVerification'; +import { Engagement } from 'models/engagement'; +import { SurveySubmission } from 'models/surveySubmission'; const FormWrapped = () => { - const { slug } = useParams(); - const { isTokenValid, isLoading, savedEngagement, submission } = useContext(ActionContext); const navigate = useNavigate(); const languagePath = `/${sessionStorage.getItem('languageId')}`; - const engagementPath = slug ? `${slug}/${languagePath}` : `engagements/${savedEngagement?.id}/view/${languagePath}`; - - if (isLoading || !savedEngagement) { - return ; - } + const [verification, slug, engagement, submission] = useAsyncValue() as [ + EmailVerification | null, + { slug: string }, + Engagement, + SurveySubmission, + ]; + const engagementPath = slug ? `${slug}/${languagePath}` : `engagements/${engagement?.id}/view/${languagePath}`; + const isTokenValid = !!verification; return ( - - + + { - { - navigate(engagementPath); - }} - /> + + + + + + ); diff --git a/met-web/src/components/survey/edit/InvalidTokenModal.tsx b/met-web/src/components/survey/edit/InvalidTokenModal.tsx deleted file mode 100644 index ea2dc70c2..000000000 --- a/met-web/src/components/survey/edit/InvalidTokenModal.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { Grid, Modal } from '@mui/material'; -import { InvalidTokenModalProps } from '../types'; -import { modalStyle, PrimaryButtonOld, MetHeader1Old, MetBodyOld } from 'components/common'; -import { useAppTranslation } from 'hooks'; - -export const InvalidTokenModal = ({ open, handleClose }: InvalidTokenModalProps) => { - const { t: translate } = useAppTranslation(); - - return ( - handleClose()} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - - - {translate('surveySubmit.inValidToken.header')} - - - - {translate('surveySubmit.inValidToken.bodyLine1')} - - - - {translate('surveySubmit.inValidToken.reasons.1')} -
- {translate('surveySubmit.inValidToken.reasons.2')} -
-
- - - {translate('surveySubmit.inValidToken.button')} - - -
-
- ); -}; diff --git a/met-web/src/components/survey/edit/index.tsx b/met-web/src/components/survey/edit/index.tsx index 74c95a22f..c29083829 100644 --- a/met-web/src/components/survey/edit/index.tsx +++ b/met-web/src/components/survey/edit/index.tsx @@ -1,12 +1,25 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import SurveySubmitWrapped from './FormWrapped'; -import { ActionProvider } from './ActionContext'; +import { Await, useLoaderData } from 'react-router-dom'; +import { Survey } from 'models/survey'; +import { EmailVerification } from 'models/emailVerification'; +import { Skeleton } from '@mui/material'; +import { Engagement } from 'models/engagement'; +import { SurveySubmission } from 'models/surveySubmission'; const SurveySubmit = () => { + const { verification, slug, engagement, submission } = useLoaderData() as { + verification: Promise; + slug: Promise<{ slug: string }>; + engagement: Engagement; + submission: SurveySubmission; + }; return ( - - - + }> + + + + ); }; diff --git a/met-web/src/components/survey/report/ReportSettingsContext.tsx b/met-web/src/components/survey/report/ReportSettingsContext.tsx deleted file mode 100644 index 8da8886e1..000000000 --- a/met-web/src/components/survey/report/ReportSettingsContext.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { createContext, useEffect, useState } from 'react'; -import { SurveyReportSetting } from 'models/surveyReportSetting'; -import { useDispatch } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { openNotification } from 'services/notificationService/notificationSlice'; -import { fetchSurveyReportSettings, updateSurveyReportSettings } from 'services/surveyService/reportSettingsService'; -import { getSurvey } from 'services/surveyService'; -import { Survey } from 'models/survey'; -import { getSlugByEngagementId } from 'services/engagementSlugService'; - -export interface SearchFilter { - key: keyof SurveyReportSetting; - value: string; -} -export interface SurveyReportSettingsContextProps { - tableLoading: boolean; - surveyReportSettings: SurveyReportSetting[]; - searchFilter: SearchFilter; - setSearchFilter: React.Dispatch>; - savingSettings: boolean; - setSavingSettings: React.Dispatch>; - handleSaveSettings: (settings: SurveyReportSetting[]) => void; - survey: Survey | null; - loadingEngagementSlug: boolean; - engagementSlug: string | null; -} - -export const ReportSettingsContext = createContext({ - tableLoading: false, - surveyReportSettings: [], - searchFilter: { key: 'question', value: '' }, - setSearchFilter: () => { - throw new Error('setSearchFilter function must be overridden'); - }, - savingSettings: false, - setSavingSettings: () => { - throw new Error('setSavingSettings function must be overridden'); - }, - handleSaveSettings: () => { - throw new Error('handleSaveSettings function must be overridden'); - }, - survey: null, - loadingEngagementSlug: false, - engagementSlug: null, -}); - -export const ReportSettingsContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { - const [tableLoading, setTableLoading] = useState(false); - const [searchFilter, setSearchFilter] = useState({ - key: 'question', - value: '', - }); - const [surveyReportSettings, setSurveyReportSettings] = useState([]); - const [savingSettings, setSavingSettings] = useState(false); - const [survey, setSurvey] = useState(null); - const [loadingSurvey, setLoadingSurvey] = useState(true); - const [loadingEngagementSlug, setLoadingEngagementSlug] = useState(true); - const [engagementSlug, setEngagementSlug] = useState(null); - - const { surveyId } = useParams<{ surveyId: string }>(); - - const dispatch = useDispatch(); - const navigate = useNavigate(); - - const loadSurveySettings = async () => { - if (!surveyId || isNaN(Number(surveyId))) { - navigate('/surveys'); - return; - } - try { - setTableLoading(true); - const settings = await fetchSurveyReportSettings(surveyId); - setSurveyReportSettings(settings); - setTableLoading(false); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while loading settings. Please try again.', - }), - ); - } - }; - - const loadSurvey = async () => { - if (!surveyId || isNaN(Number(surveyId))) { - return; - } - try { - const settings = await getSurvey(Number(surveyId)); - setSurvey(settings); - setLoadingSurvey(false); - } catch (error) { - setLoadingSurvey(false); - } - }; - - const loadEngagementSlug = async () => { - if (!survey) { - dispatch(openNotification({ severity: 'error', text: 'Failed to load dashboard link.' })); - return; - } - - if (!survey.engagement_id) { - setLoadingEngagementSlug(false); - return; - } - - try { - const slug = await getSlugByEngagementId(survey.engagement_id); - setEngagementSlug(slug.slug); - setLoadingEngagementSlug(false); - } catch (error) { - setLoadingEngagementSlug(false); - dispatch(openNotification({ severity: 'error', text: 'Failed to load dashboard link.' })); - } - }; - - useEffect(() => { - loadSurveySettings(); - loadSurvey(); - }, [surveyId]); - - useEffect(() => { - if (!loadingSurvey) { - loadEngagementSlug(); - } - }, [loadingSurvey]); - - const handleNavigateOnSave = () => { - if (survey?.engagement_id) { - navigate(`/engagements/${survey.engagement_id}/form`); - return; - } - navigate(`/surveys`); - }; - const handleSaveSettings = async (settings: SurveyReportSetting[]) => { - if (!surveyId || isNaN(Number(surveyId))) { - setSavingSettings(false); - return; - } - - if (!settings.length) { - handleNavigateOnSave(); - return; - } - - try { - await updateSurveyReportSettings(surveyId, settings); - setSavingSettings(false); - dispatch( - openNotification({ - severity: 'success', - text: 'Settings saved successfully.', - }), - ); - handleNavigateOnSave(); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while saving settings. Please try again.', - }), - ); - setSavingSettings(false); - } - }; - - return ( - - {children} - - ); -}; diff --git a/met-web/src/components/survey/report/SearchBar.tsx b/met-web/src/components/survey/report/SearchBar.tsx index 36957c070..6e26cda18 100644 --- a/met-web/src/components/survey/report/SearchBar.tsx +++ b/met-web/src/components/survey/report/SearchBar.tsx @@ -1,40 +1,47 @@ -import React, { useContext } from 'react'; -import { Stack, TextField } from '@mui/material'; -import { PrimaryButtonOld } from 'components/common'; +import React, { useState } from 'react'; +import { InputAdornment, Stack } from '@mui/material'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMagnifyingGlass } from '@fortawesome/pro-regular-svg-icons/faMagnifyingGlass'; -import { ReportSettingsContext } from './ReportSettingsContext'; - -const SearchBar = () => { - const { searchFilter, setSearchFilter } = useContext(ReportSettingsContext); - const [searchText, setSearchText] = React.useState(''); +import { Button, TextField } from 'components/common/Input'; +const SearchBar = ({ + searchTerm, + setSearchTerm, +}: { + searchTerm: string; + setSearchTerm: (searchTerm: string) => void; +}) => { + const [searchValue, setSearchValue] = useState(searchTerm); return ( <> - + { - setSearchText(e.target.value); + value={searchValue} + onChange={setSearchValue} + onKeyDown={(e) => { + if (e.key !== 'Enter') return; + setSearchTerm(searchValue); }} size="small" + endAdornment={ + + + + } /> - { - setSearchFilter({ - ...searchFilter, - value: searchText, - }); - }} - > - - ); diff --git a/met-web/src/components/survey/report/SettingsForm.tsx b/met-web/src/components/survey/report/SettingsForm.tsx index 8bde1c3eb..212bb3009 100644 --- a/met-web/src/components/survey/report/SettingsForm.tsx +++ b/met-web/src/components/survey/report/SettingsForm.tsx @@ -1,31 +1,106 @@ -import React, { useContext, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { ClickAwayListener, Grid, Stack, InputAdornment, TextField, Tooltip } from '@mui/material'; -import { - MetHeader3, - MetLabel, - MetPageGridContainer, - MetPaper, - PrimaryButtonOld, - SecondaryButtonOld, -} from 'components/common'; +import React, { Suspense, useState } from 'react'; +import { Await, useAsyncValue, useNavigate, useRouteLoaderData } from 'react-router-dom'; +import { ClickAwayListener, Grid, Stack, InputAdornment, Tooltip, Skeleton } from '@mui/material'; +import { MetHeader3, MetLabel, MetPageGridContainer, MetPaper } from 'components/common'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy } from '@fortawesome/pro-regular-svg-icons/faCopy'; -import { ReportSettingsContext } from './ReportSettingsContext'; import SettingsTable from './SettingsTable'; import SearchBar from './SearchBar'; import { getBaseUrl } from 'helper'; +import { Button, FormField, TextInput } from 'components/common/Input'; +import { Survey } from 'models/survey'; +import { SurveyReportSetting } from 'models/surveyReportSetting'; +import MetTable from 'components/common/Table'; +import { Engagement } from 'models/engagement'; +import { updateSurveyReportSettings } from 'services/surveyService/reportSettingsService'; +import { useAppDispatch } from 'hooks'; +import { openNotification } from 'services/notificationService/notificationSlice'; + +const SettingsFormPage = () => { + const { survey, slug, engagement } = useRouteLoaderData('survey') as { + survey: Promise; + slug: Promise<{ slug: string }>; + engagement: Promise; + }; + + return ( + + + Report Settings + + + + + + + + + + + + ); +}; const SettingsForm = () => { - const { setSavingSettings, savingSettings, engagementSlug, loadingEngagementSlug, survey } = - useContext(ReportSettingsContext); + const [survey, slug] = useAsyncValue() as [Survey, { slug: string }, SurveyReportSetting[]]; + const [searchTerm, setSearchTerm] = useState(''); + const { reportSettings } = useRouteLoaderData('survey') as { reportSettings: Promise }; + + const engagementSlug = slug?.slug; const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const [displayedSettings, setDisplayedSettings] = useState<{ [key: number]: boolean }>({}); const [copyTooltip, setCopyTooltip] = useState(false); + const handleNavigateOnSave = () => { + if (survey?.engagement_id) { + navigate(`/engagements/${survey.engagement_id}/form`); + return; + } + navigate(`/surveys`); + }; + + const handleSaveSettings = async () => { + const surveyReportSettings = await reportSettings; // Should resolve immediately + const updatedSettings = surveyReportSettings.map((setting) => { + return { + ...setting, + display: displayedSettings[setting.id], + }; + }); + + if (!surveyReportSettings.length) { + handleNavigateOnSave(); + return; + } + + try { + await updateSurveyReportSettings(survey.id.toString(), updatedSettings); + dispatch( + openNotification({ + severity: 'success', + text: 'Settings saved successfully.', + }), + ); + handleNavigateOnSave(); + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while saving settings. Please try again.', + }), + ); + } + }; + const baseUrl = getBaseUrl(); - const engagementUrl = !survey?.engagement_id + const engagementUrl = !engagementSlug ? 'Link will appear when the survey is linked to an engagement' : `${baseUrl}/${engagementSlug}/dashboard/public`; @@ -40,99 +115,118 @@ const SettingsForm = () => { }; return ( - + + + + +
+ + + + + + ) + } + /> +
+
+
+
+ + + - Report Settings + Select the questions you would like to display on the public report - - - - Link to Public Dashboard Report - - - - - - - - - ), - }} - /> - - - - - Select the questions you would like to display on the public report - - - - - - - - - - setSavingSettings(true)} - loading={savingSettings} - > - Save - - navigate(`/surveys/${survey?.id}/build`)}> - Back - - - - - + }> + + + + -
+ + + + + + + ); }; -export default SettingsForm; +const settingsFormSkeleton = ( + + + + + + + + + + + + + Select the questions you would like to display on the public report + + + + + +); + +export default SettingsFormPage; diff --git a/met-web/src/components/survey/report/SettingsTable.tsx b/met-web/src/components/survey/report/SettingsTable.tsx index 68b7f1222..8d7c5e4d7 100644 --- a/met-web/src/components/survey/report/SettingsTable.tsx +++ b/met-web/src/components/survey/report/SettingsTable.tsx @@ -1,16 +1,25 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Checkbox } from '@mui/material'; import { HeadCell } from 'components/common/Table/types'; import MetTable from 'components/common/Table'; import { ClientSidePagination } from 'components/common/Table/ClientSidePagination'; import { SurveyReportSetting } from 'models/surveyReportSetting'; -import { ReportSettingsContext } from './ReportSettingsContext'; -import { updatedDiff } from 'deep-object-diff'; +import { useAsyncValue } from 'react-router-dom'; -const SettingsTable = () => { - const { surveyReportSettings, searchFilter, savingSettings, handleSaveSettings, tableLoading } = - useContext(ReportSettingsContext); - const [displayedMap, setDisplayedMap] = useState<{ [key: number]: boolean }>({}); +const SettingsTable = ({ + displayedMap, + setDisplayedMap, + searchTerm, +}: { + displayedMap: { [key: number]: boolean }; + setDisplayedMap: React.Dispatch< + React.SetStateAction<{ + [key: number]: boolean; + }> + >; + searchTerm: string; +}) => { + const surveyReportSettings = useAsyncValue() as SurveyReportSetting[]; useEffect(() => { const map = surveyReportSettings.reduce((acc, curr) => { @@ -20,23 +29,6 @@ const SettingsTable = () => { setDisplayedMap(map); }, [surveyReportSettings]); - useEffect(() => { - if (!savingSettings) { - return; - } - const updatedSettings = surveyReportSettings.map((setting) => { - return { - ...setting, - display: displayedMap[setting.id], - }; - }); - const diff = updatedDiff(surveyReportSettings, updatedSettings); - const diffKeys = Object.keys(diff); - const updatedDiffSettings = diffKeys.map((key) => updatedSettings[Number(key)]); - - handleSaveSettings(updatedDiffSettings); - }, [savingSettings]); - const headCells: HeadCell[] = [ { key: 'id', @@ -76,8 +68,8 @@ const SettingsTable = () => { ]; return ( - - {(props) => } + + {(props) => } ); }; diff --git a/met-web/src/components/survey/report/index.tsx b/met-web/src/components/survey/report/index.tsx index f2b58dbca..c83eaa530 100644 --- a/met-web/src/components/survey/report/index.tsx +++ b/met-web/src/components/survey/report/index.tsx @@ -1,13 +1,8 @@ import React from 'react'; import SettingsForm from './SettingsForm'; -import { ReportSettingsContextProvider } from './ReportSettingsContext'; const ReportSettings = () => { - return ( - - - - ); + return ; }; export default ReportSettings; diff --git a/met-web/src/components/survey/submit/ActionContext.tsx b/met-web/src/components/survey/submit/ActionContext.tsx deleted file mode 100644 index 8d72f7e28..000000000 --- a/met-web/src/components/survey/submit/ActionContext.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React, { createContext, useState, useEffect } from 'react'; -import { AxiosError } from 'axios'; -import { createDefaultSurvey, Survey } from 'models/survey'; -import { useAppDispatch, useAppSelector } from 'hooks'; -import { useNavigate, useParams } from 'react-router-dom'; -import { openNotification } from 'services/notificationService/notificationSlice'; -import { SurveyParams } from '../types'; -import { getEmailVerification } from 'services/emailVerificationService'; -import { getEngagement } from 'services/engagementService'; -import { getSurvey } from 'services/surveyService'; -import { submitSurvey } from 'services/submissionService'; -import { Engagement, createDefaultEngagement } from 'models/engagement'; -import { getSlugByEngagementId } from 'services/engagementSlugService'; -import { useAppTranslation } from 'hooks'; - -interface SubmitSurveyContext { - savedSurvey: Survey; - isSurveyLoading: boolean; - token?: string; - isTokenValid: boolean; - handleSubmit: (submissionData: unknown) => void; - isSubmitting: boolean; - savedEngagement: Engagement | null; - isEngagementLoading: boolean; - loadEngagement: null | (() => void); - slug: string; -} - -export const ActionContext = createContext({ - savedSurvey: createDefaultSurvey(), - isSurveyLoading: true, - isTokenValid: true, - handleSubmit: (_submissionData: unknown) => { - return; - }, - isSubmitting: false, - savedEngagement: null, - isEngagementLoading: true, - loadEngagement: null, - slug: '', -}); - -export const ActionProvider = ({ children }: { children: JSX.Element }) => { - const { t: translate } = useAppTranslation(); - const languagePath = `/${sessionStorage.getItem('languageId')}`; - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); - const { surveyId, token } = useParams(); - const [savedSurvey, setSavedSurvey] = useState(createDefaultSurvey()); - const [isSurveyLoading, setIsSurveyLoading] = useState(true); - const [isTokenValid, setTokenValid] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); - const [savedEngagement, setSavedEngagement] = useState(null); - const [isEngagementLoading, setIsEngagementLoading] = useState(true); - const [slug, setSlug] = useState(''); - - const verifyToken = async () => { - if (isLoggedIn) { - setIsSurveyLoading(false); - return; - } - - if (!token) { - navigate(`/not-found`); - return; - } - - try { - const verification = await getEmailVerification(token); - if (!verification || verification.survey_id !== Number(surveyId)) { - throw new Error(translate('surveySubmit.surveySubmitNotification.verificationError')); - } - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveySubmit.surveySubmitNotification.invalidToken'), - }), - ); - setTokenValid(false); - } finally { - setIsSurveyLoading(false); - } - }; - - useEffect(() => { - loadSurvey(); - }, []); - const loadSurvey = async () => { - if (isNaN(Number(surveyId))) { - navigate('/not-found'); - dispatch( - openNotification({ - severity: 'error', - text: translate('surveySubmit.surveySubmitNotification.invalidSurvey'), - }), - ); - return; - } - try { - const loadedSurvey = await getSurvey(Number(surveyId)); - setSavedSurvey(loadedSurvey); - setIsSurveyLoading(false); - verifyToken(); - } catch (error) { - if ((error as AxiosError)?.response?.status === 500) { - navigate('/not-available'); - } else { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveySubmit.surveySubmitNotification.surveyError'), - }), - ); - navigate(`/not-available`); - } - } - }; - - useEffect(() => { - if (savedSurvey?.id !== 0) { - loadEngagement(); - } - }, [savedSurvey]); - const loadEngagement = async () => { - if (isNaN(Number(savedSurvey.engagement_id)) || !savedSurvey.engagement_id) { - setSavedEngagement({ - ...createDefaultEngagement(), - name: savedSurvey.name, - }); - setIsEngagementLoading(false); - return; - } - - setIsEngagementLoading(true); - try { - const loadedEngagement = await getEngagement(Number(savedSurvey.engagement_id)); - setSavedEngagement(loadedEngagement); - setIsEngagementLoading(false); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveySubmit.surveySubmitNotification.engagementError'), - }), - ); - setIsEngagementLoading(false); - } - }; - - const handleFetchSlug = async () => { - if (!savedSurvey.engagement_id) { - return; - } - try { - const response = await getSlugByEngagementId(savedSurvey.engagement_id); - setSlug(response.slug); - } catch (error) { - setSlug(''); - } - }; - - useEffect(() => { - handleFetchSlug(); - }, [savedSurvey.engagement_id]); - - const handleSubmit = async (submissionData: unknown) => { - try { - setIsSubmitting(true); - await submitSurvey({ - survey_id: savedSurvey.id, - submission_json: submissionData, - verification_token: token ? token : '', - }); - - try { - window.snowplow('trackSelfDescribingEvent', { - schema: 'iglu:ca.bc.gov.met/submit-survey/jsonschema/1-0-0', - data: { survey_id: savedSurvey.id, engagement_id: savedSurvey.engagement_id }, - }); - } catch (error) { - console.log('Survey submit notification snowplow error:', error); - } - dispatch( - openNotification({ - severity: 'success', - text: translate('surveySubmit.surveySubmitNotification.success'), - }), - ); - navigate(`/${slug}/${languagePath}`, { - state: { - open: true, - }, - }); - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: translate('surveySubmit.surveySubmitNotification.submissionError'), - }), - ); - setIsSubmitting(false); - verifyToken(); - } - }; - - return ( - - {children} - - ); -}; diff --git a/met-web/src/components/survey/submit/EngagementLink.tsx b/met-web/src/components/survey/submit/EngagementLink.tsx index e50ed6902..201653b6e 100644 --- a/met-web/src/components/survey/submit/EngagementLink.tsx +++ b/met-web/src/components/survey/submit/EngagementLink.tsx @@ -1,16 +1,16 @@ -import React, { useContext, useCallback } from 'react'; -import { Link as MuiLink, Skeleton } from '@mui/material'; -import { Link, useNavigate } from 'react-router-dom'; +import React, { useCallback } from 'react'; +import { Link as MuiLink } from '@mui/material'; +import { Link, useAsyncValue, useNavigate } from 'react-router-dom'; import { useAppSelector } from 'hooks'; -import { ActionContext } from './ActionContext'; import { When } from 'react-if'; import { useDispatch } from 'react-redux'; import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; +import { Engagement } from 'models/engagement'; export const EngagementLink = () => { const dispatch = useDispatch(); - const { savedEngagement, isEngagementLoading } = useContext(ActionContext); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); + const engagement = useAsyncValue() as Engagement | null; const navigate = useNavigate(); const handleNavigate = useCallback( @@ -45,17 +45,9 @@ export const EngagementLink = () => { [dispatch, navigate], ); - if (isEngagementLoading) { - return ; - } - - if (!savedEngagement) { - return null; - } - return ( <> - + { handleNavigate('/surveys'); }} > - {`<< Return to survey list`} + << Return to survey list diff --git a/met-web/src/components/survey/submit/InvalidTokenModal.tsx b/met-web/src/components/survey/submit/InvalidTokenModal.tsx index 1947f22b6..3dfb44f64 100644 --- a/met-web/src/components/survey/submit/InvalidTokenModal.tsx +++ b/met-web/src/components/survey/submit/InvalidTokenModal.tsx @@ -1,16 +1,38 @@ import React from 'react'; import { Grid, Modal } from '@mui/material'; -import { InvalidTokenModalProps } from '../types'; -import { modalStyle, PrimaryButtonOld, MetHeader1Old, MetBodyOld } from 'components/common'; -import { useAppTranslation } from 'hooks'; +import { modalStyle } from 'components/common'; +import { useAppSelector, useAppTranslation } from 'hooks'; +import { useAsyncValue, useNavigate } from 'react-router-dom'; +import { EmailVerification } from 'models/emailVerification'; +import { Button } from 'components/common/Input'; +import { BodyText } from 'components/common/Typography'; -export const InvalidTokenModal = ({ open, handleClose }: InvalidTokenModalProps) => { +interface PromiseResult { + status: 'fulfilled' | 'rejected'; + value?: T; + reason?: any; +} + +export const InvalidTokenModal = () => { const { t: translate } = useAppTranslation(); + const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); + const navigate = useNavigate(); + const [verificationResult, slugResult] = useAsyncValue() as [ + PromiseResult, + PromiseResult<{ slug: string }>, + ]; + const verification = verificationResult?.value; + const slug = slugResult.value?.slug ?? ''; + const languagePath = `/${sessionStorage.getItem('languageId')}`; + + const navigateToEngagement = () => { + navigate(`/${slug}/${languagePath}`); + }; return ( handleClose()} + open={!verification && !isLoggedIn} + onClose={navigateToEngagement} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" > @@ -23,26 +45,26 @@ export const InvalidTokenModal = ({ open, handleClose }: InvalidTokenModalProps) spacing={2} > - + {translate('surveySubmit.inValidToken.header')} - + - {translate('surveySubmit.inValidToken.bodyLine1')} + {translate('surveySubmit.inValidToken.bodyLine1')} - - {translate('surveySubmit.inValidToken.reasons.0')} -
- {translate('surveySubmit.inValidToken.reasons.1')} -
- {translate('surveySubmit.inValidToken.reasons.2')} -
+ +
    +
  • {translate('surveySubmit.inValidToken.reasons.0')}
  • +
  • {translate('surveySubmit.inValidToken.reasons.1')}
  • +
  • {translate('surveySubmit.inValidToken.reasons.2')}
  • +
+
- +
diff --git a/met-web/src/components/survey/submit/PreviewBanner.tsx b/met-web/src/components/survey/submit/PreviewBanner.tsx index 5aa560ffa..3c9700da4 100644 --- a/met-web/src/components/survey/submit/PreviewBanner.tsx +++ b/met-web/src/components/survey/submit/PreviewBanner.tsx @@ -1,51 +1,44 @@ -import React, { useContext } from 'react'; -import { Box, Grid, Stack } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { MetHeader1Old, SecondaryButtonOld } from 'components/common'; +import React, { Suspense } from 'react'; +import { Box, Grid, Skeleton, Stack } from '@mui/material'; +import { useNavigate, Await, useRouteLoaderData } from 'react-router-dom'; import { useAppSelector } from 'hooks'; -import { ActionContext } from './ActionContext'; import { PermissionsGate } from 'components/permissionsGate'; import { USER_ROLES } from 'services/userService/constants'; +import { Header1 } from 'components/common/Typography'; +import { Button } from 'components/common/Input'; +import { Survey } from 'models/survey'; export const PreviewBanner = () => { const navigate = useNavigate(); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); - const { savedSurvey } = useContext(ActionContext); + const { survey } = useRouteLoaderData('survey') as { survey: Promise }; - if (!isLoggedIn) { + if (!isLoggedIn || !survey) { return null; } - if (!savedSurvey) { - return null; - } - - return ( - + const Banner = (survey: Survey) => ( + - Preview Survey + Preview Survey - + - navigate(`/surveys/${savedSurvey.id}/build`)} - > + ); + + return ( + }> + {Banner} + + ); }; diff --git a/met-web/src/components/survey/submit/SurveyBanner.tsx b/met-web/src/components/survey/submit/SurveyBanner.tsx index eb3160cad..32eaff03b 100644 --- a/met-web/src/components/survey/submit/SurveyBanner.tsx +++ b/met-web/src/components/survey/submit/SurveyBanner.tsx @@ -1,46 +1,17 @@ -import React, { useContext } from 'react'; -import { Box, Grid, IconButton, Skeleton, Stack } from '@mui/material'; +import React from 'react'; import { Banner } from 'components/banner/Banner'; -import { ActionContext } from './ActionContext'; -import ReplayIcon from '@mui/icons-material/Replay'; -import { MetHeader4 } from 'components/common'; import EngagementInfoSection from 'components/engagement/view/EngagementInfoSection'; +import { Engagement } from 'models/engagement'; +import { useAsyncValue } from 'react-router-dom'; export const SurveyBanner = () => { - const { isEngagementLoading, savedEngagement, loadEngagement } = useContext(ActionContext); + const engagement = useAsyncValue() as Engagement | undefined; - if (isEngagementLoading) { - return ; - } - - if (!savedEngagement) { - return ( - - - - Could not load banner, press to try again - { - if (loadEngagement) loadEngagement(); - }} - > - - - - - - ); - } + if (!engagement) return null; return ( - - + + ); }; diff --git a/met-web/src/components/survey/submit/SurveyForm.tsx b/met-web/src/components/survey/submit/SurveyForm.tsx index 94f3a6ee3..5ae62ea26 100644 --- a/met-web/src/components/survey/submit/SurveyForm.tsx +++ b/met-web/src/components/survey/submit/SurveyForm.tsx @@ -1,29 +1,112 @@ -import React, { useContext, useState } from 'react'; -import { Skeleton, Grid, Stack } from '@mui/material'; -import { ActionContext } from './ActionContext'; +import React, { useEffect, useRef, useState } from 'react'; +import { Grid, Stack } from '@mui/material'; import FormSubmit from 'components/Form/FormSubmit'; import { FormSubmissionData } from 'components/Form/types'; -import { useAppSelector } from 'hooks'; -import { PrimaryButtonOld, SecondaryButtonOld } from 'components/common'; -import { SurveyFormProps } from '../types'; +import { useAppDispatch, useAppSelector } from 'hooks'; import { When } from 'react-if'; import { useAppTranslation } from 'hooks'; +import { submitSurvey } from 'services/submissionService'; +import { useAsyncValue, useBlocker, useNavigate } from 'react-router-dom'; +import { EmailVerification } from 'models/emailVerification'; +import { Survey } from 'models/survey'; +import { Button } from 'components/common/Input'; +import { openNotification } from 'services/notificationService/notificationSlice'; +import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; -export const SurveyForm = ({ handleClose }: SurveyFormProps) => { +export const SurveyForm = () => { const { t: translate } = useAppTranslation(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); - const { isSurveyLoading, savedSurvey, handleSubmit, isSubmitting } = useContext(ActionContext); + const languagePath = `/${sessionStorage.getItem('languageId')}`; const [submissionData, setSubmissionData] = useState(null); + + const initialSet = useRef(false); // Track if the initial state has been set const [isValid, setIsValid] = useState(false); + const [isChanged, setIsChanged] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [survey, verification, slug] = useAsyncValue() as [Survey, EmailVerification | null, { slug: string }]; + + const token = verification?.verification_token; const handleChange = (filledForm: FormSubmissionData) => { + if (!initialSet.current) { + console.log('setting initial state'); + initialSet.current = true; + } else { + setIsChanged(true); + } setSubmissionData(filledForm.data); setIsValid(filledForm.isValid); }; - if (isSurveyLoading) { - return ; - } + const navigateToEngagement = () => { + navigate(`/${slug.slug}/${languagePath}`); + }; + + const handleSubmit = async (submissionData: unknown) => { + setIsSubmitting(true); + try { + await submitSurvey({ + survey_id: survey.id, + submission_json: submissionData, + verification_token: token ?? '', + }); + + try { + window.snowplow('trackSelfDescribingEvent', { + schema: 'iglu:ca.bc.gov.met/submit-survey/jsonschema/1-0-0', + data: { survey_id: survey.id, engagement_id: survey.engagement_id }, + }); + } catch (error) { + console.log('Survey submit notification snowplow error:', error); + } + dispatch( + openNotification({ + severity: 'success', + text: translate('surveySubmit.surveySubmitNotification.success'), + }), + ); + navigate(`/${slug.slug}/${languagePath}`, { + state: { + open: true, + }, + }); + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: translate('surveySubmit.surveySubmitNotification.submissionError'), + }), + ); + } + }; + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => + isChanged && !isLoggedIn && !isSubmitting && nextLocation.pathname !== currentLocation.pathname, + ); + useEffect(() => { + if (blocker.state === 'blocked') { + dispatch( + openNotificationModal({ + open: true, + data: { + style: 'warning', + header: 'Unsaved Changes', + subHeader: + 'If you leave this page, your changes will not be saved. Are you sure you want to leave this page?', + subText: [], + confirmButtonText: 'Leave', + cancelButtonText: 'Stay', + handleConfirm: blocker.proceed, + handleClose: blocker.reset, + }, + type: 'confirm', + }), + ); + } + }, [blocker, dispatch]); return ( { spacing={1} padding={'2em 2em 1em 2em'} > + {isChanged && JSON.stringify(submissionData)} - + { width="100%" justifyContent="flex-end" > - handleClose()}> + diff --git a/met-web/src/components/survey/submit/SurveySubmitWrapped.tsx b/met-web/src/components/survey/submit/SurveySubmitWrapped.tsx deleted file mode 100644 index 7f61f7c88..000000000 --- a/met-web/src/components/survey/submit/SurveySubmitWrapped.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useContext } from 'react'; -import { Grid } from '@mui/material'; -import { SurveyBanner } from './SurveyBanner'; -import { SurveyForm } from './SurveyForm'; -import { ActionContext } from './ActionContext'; -import { MetPaper } from 'components/common'; -import { InvalidTokenModal } from './InvalidTokenModal'; -import { useNavigate } from 'react-router'; -import { EngagementLink } from './EngagementLink'; -import { When } from 'react-if'; -import { PreviewBanner } from './PreviewBanner'; - -const SurveySubmitWrapped = () => { - const { savedSurvey, isTokenValid, slug } = useContext(ActionContext); - const languagePath = `/${sessionStorage.getItem('languageId')}`; - const navigate = useNavigate(); - return ( - - - - - - - - - - - - - - - { - navigate(`/${slug}/${languagePath}`); - }} - /> - - { - navigate(`/${slug}/${languagePath}`); - }} - /> - - - - - ); -}; - -export default SurveySubmitWrapped; diff --git a/met-web/src/components/survey/submit/index.tsx b/met-web/src/components/survey/submit/index.tsx index 9660e1892..4228acdcc 100644 --- a/met-web/src/components/survey/submit/index.tsx +++ b/met-web/src/components/survey/submit/index.tsx @@ -1,12 +1,67 @@ -import React from 'react'; -import SurveySubmitWrapped from './SurveySubmitWrapped'; -import { ActionProvider } from './ActionContext'; +import React, { Suspense } from 'react'; +import { Grid, Skeleton } from '@mui/material'; +import { SurveyBanner } from './SurveyBanner'; +import { SurveyForm } from './SurveyForm'; +import { MetPaper } from 'components/common'; +import { InvalidTokenModal } from './InvalidTokenModal'; +import { EngagementLink } from './EngagementLink'; +import { PreviewBanner } from './PreviewBanner'; +import { Survey } from 'models/survey'; +import { useRouteLoaderData, Await } from 'react-router-dom'; +import { EmailVerification } from 'models/emailVerification'; +import { Engagement } from 'models/engagement'; const SurveySubmit = () => { + const { survey, slug, verification, engagement } = useRouteLoaderData('survey') as { + survey: Promise; + slug: Promise<{ slug: string }>; + verification: Promise; + engagement: Engagement; + }; return ( - - - + + + + + + }> + + + + + + + + }> + + + + + + + + }> + + + + + + + + + + + + + ); }; diff --git a/met-web/src/components/survey/types.ts b/met-web/src/components/survey/types.ts index 36aed2546..def8fce1e 100644 --- a/met-web/src/components/survey/types.ts +++ b/met-web/src/components/survey/types.ts @@ -9,11 +9,6 @@ export interface FormBuilderForm { components: unknown[]; } -export interface InvalidTokenModalProps { - open: boolean; - handleClose: () => void; -} - export interface SurveyFormProps { handleClose: () => void; } diff --git a/met-web/src/models/emailVerification.ts b/met-web/src/models/emailVerification.ts index 5dc750ec5..eb5057909 100644 --- a/met-web/src/models/emailVerification.ts +++ b/met-web/src/models/emailVerification.ts @@ -3,6 +3,7 @@ export interface EmailVerification { survey_id: number; type: EmailVerificationType; participant_id: number; + verification_token?: string; } export enum EmailVerificationType { diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 3df0a1125..efc6925ab 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -30,6 +30,7 @@ import TenantDetail from 'components/tenantManagement/Detail'; import Language from 'components/language'; import { Tenant } from 'models/tenant'; import { getAllTenants, getTenant } from 'services/tenantService'; +import { SurveyLoader } from 'components/survey/building/SurveyLoader'; const AuthenticatedRoutes = () => { return ( @@ -39,9 +40,11 @@ const AuthenticatedRoutes = () => { } /> } /> - } /> - } /> - } /> + } id="survey" loader={SurveyLoader}> + } /> + } /> + } /> + }> } /> } /> diff --git a/met-web/src/routes/UnauthenticatedRoutes.tsx b/met-web/src/routes/UnauthenticatedRoutes.tsx index b0945847a..5f9128799 100644 --- a/met-web/src/routes/UnauthenticatedRoutes.tsx +++ b/met-web/src/routes/UnauthenticatedRoutes.tsx @@ -13,6 +13,7 @@ import withLanguageParam from './LanguageParam'; import { Navigate, Route } from 'react-router-dom'; import NotFound from './NotFound'; import ViewEngagement, { engagementLoader } from 'components/engagement/new/view'; +import { SurveyLoader } from 'components/survey/building/SurveyLoader'; const ManageSubscriptionWrapper = withLanguageParam(ManageSubscription); const EngagementViewWrapper = withLanguageParam(EngagementView); @@ -27,7 +28,12 @@ const UnauthenticatedRoutes = () => { return ( <> } /> - } /> + } + /> } /> { } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -53,7 +59,7 @@ const UnauthenticatedRoutes = () => { } /> } /> - } /> + } /> } /> } /> From e2e08f925754359110662439685a1593ba6f7080 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 15:04:49 -0700 Subject: [PATCH 2/6] Fix lint errors and unit tests --- .../components/common/Typography/Headers.tsx | 1 - .../survey/building/SurveyLoader.tsx | 3 +- .../src/components/survey/building/index.tsx | 6 +- .../components/survey/edit/FormWrapped.tsx | 6 +- met-web/src/components/survey/edit/index.tsx | 1 - .../components/survey/report/SettingsForm.tsx | 16 +-- .../src/components/survey/report/index.tsx | 7 +- .../survey/submit/InvalidTokenModal.tsx | 1 - .../components/survey/submit/SurveyForm.tsx | 1 - .../survey/SurveyReportSettings.test.tsx | 102 +++++++++++------- 10 files changed, 78 insertions(+), 66 deletions(-) diff --git a/met-web/src/components/common/Typography/Headers.tsx b/met-web/src/components/common/Typography/Headers.tsx index 4d8e328b5..869fa4d82 100644 --- a/met-web/src/components/common/Typography/Headers.tsx +++ b/met-web/src/components/common/Typography/Headers.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Typography, TypographyProps } from '@mui/material'; -import { Palette } from 'styles/Theme'; const fontWeight = (weight?: string | number) => { switch (weight) { diff --git a/met-web/src/components/survey/building/SurveyLoader.tsx b/met-web/src/components/survey/building/SurveyLoader.tsx index fad3a662a..caec70654 100644 --- a/met-web/src/components/survey/building/SurveyLoader.tsx +++ b/met-web/src/components/survey/building/SurveyLoader.tsx @@ -1,9 +1,8 @@ -import { EmailVerification } from 'models/emailVerification'; import { Params, defer } from 'react-router-dom'; import { getEmailVerification } from 'services/emailVerificationService'; import { getEngagement } from 'services/engagementService'; import { getSlugByEngagementId } from 'services/engagementSlugService'; -import { getSubmission, getSubmissionByToken } from 'services/submissionService'; +import { getSubmissionByToken } from 'services/submissionService'; import { getSurvey } from 'services/surveyService'; import { fetchSurveyReportSettings } from 'services/surveyService/reportSettingsService'; diff --git a/met-web/src/components/survey/building/index.tsx b/met-web/src/components/survey/building/index.tsx index 00ccf0a45..8c03abfd3 100644 --- a/met-web/src/components/survey/building/index.tsx +++ b/met-web/src/components/survey/building/index.tsx @@ -29,7 +29,7 @@ import { PermissionsGate } from 'components/permissionsGate'; import { USER_ROLES } from 'services/userService/constants'; import axios from 'axios'; import { AutoSaveSnackBar } from './AutoSaveSnackBar'; -import { debounce, set } from 'lodash'; +import { debounce } from 'lodash'; import { Button } from 'components/common/Input'; import { Controller, useForm } from 'react-hook-form'; import { @@ -63,7 +63,7 @@ export const FormBuilderPage = () => { const { control, - formState: { isSubmitting, isSubmitted, isDirty }, + formState: { isSubmitting, isDirty }, handleSubmit, reset, watch, @@ -116,7 +116,7 @@ export const FormBuilderPage = () => { const autoSaveForm = async (formDef: FormBuilderData) => { try { await handleSubmit(async (data: Omit) => { - const { form_json, ...result } = await putSurvey({ + const result = await putSurvey({ ...data, form_json: formDef, }); diff --git a/met-web/src/components/survey/edit/FormWrapped.tsx b/met-web/src/components/survey/edit/FormWrapped.tsx index 883f8340c..f7e44a3fb 100644 --- a/met-web/src/components/survey/edit/FormWrapped.tsx +++ b/met-web/src/components/survey/edit/FormWrapped.tsx @@ -1,12 +1,12 @@ -import React, { Suspense, useContext } from 'react'; -import { Grid, Skeleton } from '@mui/material'; +import React, { Suspense } from 'react'; +import { Grid } from '@mui/material'; import { Banner } from 'components/banner/Banner'; import { EditForm } from './EditForm'; import { MetPaper } from 'components/common'; import { InvalidTokenModal } from '../submit/InvalidTokenModal'; import { When } from 'react-if'; import EngagementInfoSection from 'components/engagement/view/EngagementInfoSection'; -import { Await, useAsyncValue, useNavigate, useParams } from 'react-router-dom'; +import { Await, useAsyncValue, useNavigate } from 'react-router-dom'; import { EmailVerification } from 'models/emailVerification'; import { Engagement } from 'models/engagement'; import { SurveySubmission } from 'models/surveySubmission'; diff --git a/met-web/src/components/survey/edit/index.tsx b/met-web/src/components/survey/edit/index.tsx index c29083829..28f655765 100644 --- a/met-web/src/components/survey/edit/index.tsx +++ b/met-web/src/components/survey/edit/index.tsx @@ -1,7 +1,6 @@ import React, { Suspense } from 'react'; import SurveySubmitWrapped from './FormWrapped'; import { Await, useLoaderData } from 'react-router-dom'; -import { Survey } from 'models/survey'; import { EmailVerification } from 'models/emailVerification'; import { Skeleton } from '@mui/material'; import { Engagement } from 'models/engagement'; diff --git a/met-web/src/components/survey/report/SettingsForm.tsx b/met-web/src/components/survey/report/SettingsForm.tsx index 212bb3009..743b93b60 100644 --- a/met-web/src/components/survey/report/SettingsForm.tsx +++ b/met-web/src/components/survey/report/SettingsForm.tsx @@ -15,6 +15,7 @@ import { Engagement } from 'models/engagement'; import { updateSurveyReportSettings } from 'services/surveyService/reportSettingsService'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; +import { updatedDiff } from 'deep-object-diff'; const SettingsFormPage = () => { const { survey, slug, engagement } = useRouteLoaderData('survey') as { @@ -29,12 +30,8 @@ const SettingsFormPage = () => { Report Settings - - + + @@ -68,12 +65,15 @@ const SettingsForm = () => { const handleSaveSettings = async () => { const surveyReportSettings = await reportSettings; // Should resolve immediately - const updatedSettings = surveyReportSettings.map((setting) => { + const currentSettings = surveyReportSettings.map((setting) => { return { ...setting, display: displayedSettings[setting.id], }; }); + const diff = updatedDiff(surveyReportSettings, currentSettings); + const diffKeys = Object.keys(diff); + const updatedSettings = diffKeys.map((key) => currentSettings[Number(key)]); if (!surveyReportSettings.length) { handleNavigateOnSave(); @@ -200,7 +200,7 @@ const SettingsForm = () => { ); }; -const settingsFormSkeleton = ( +const SettingsFormSkeleton = ( diff --git a/met-web/src/components/survey/report/index.tsx b/met-web/src/components/survey/report/index.tsx index c83eaa530..796418db9 100644 --- a/met-web/src/components/survey/report/index.tsx +++ b/met-web/src/components/survey/report/index.tsx @@ -1,8 +1,3 @@ -import React from 'react'; import SettingsForm from './SettingsForm'; -const ReportSettings = () => { - return ; -}; - -export default ReportSettings; +export default SettingsForm; diff --git a/met-web/src/components/survey/submit/InvalidTokenModal.tsx b/met-web/src/components/survey/submit/InvalidTokenModal.tsx index 3dfb44f64..957d7c31a 100644 --- a/met-web/src/components/survey/submit/InvalidTokenModal.tsx +++ b/met-web/src/components/survey/submit/InvalidTokenModal.tsx @@ -10,7 +10,6 @@ import { BodyText } from 'components/common/Typography'; interface PromiseResult { status: 'fulfilled' | 'rejected'; value?: T; - reason?: any; } export const InvalidTokenModal = () => { diff --git a/met-web/src/components/survey/submit/SurveyForm.tsx b/met-web/src/components/survey/submit/SurveyForm.tsx index 5ae62ea26..a3eb2ceef 100644 --- a/met-web/src/components/survey/submit/SurveyForm.tsx +++ b/met-web/src/components/survey/submit/SurveyForm.tsx @@ -117,7 +117,6 @@ export const SurveyForm = () => { spacing={1} padding={'2em 2em 1em 2em'} > - {isChanged && JSON.stringify(submissionData)} ({ ...jest.requireActual('hooks'), @@ -58,17 +57,37 @@ jest.mock('@mui/material', () => ({ useMediaQuery: jest.fn(() => true), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(() => { + return { surveyId: '1' }; + }), + useRouteLoaderData: jest.fn(() => ({ + survey: Promise.resolve(survey), + slug: Promise.resolve(engagementSlug), + reportSettings: Promise.resolve(SurveyReportSettings), + engagement: Promise.resolve(draftEngagement), + })), +})); + +const router = createMemoryRouter( + [ + { + path: '/surveys/:surveyId/report', + element: , + id: 'survey', + }, + ], + { + initialEntries: ['/surveys/1/report'], + }, +); + describe('Survey report settings tests', () => { jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => jest.fn()); jest.spyOn(reactRedux, 'useDispatch').mockImplementation(() => jest.fn()); jest.spyOn(reactRouter, 'useNavigate').mockImplementation(() => jest.fn()); jest.spyOn(reactRouter, 'useParams').mockReturnValue({ surveyId: String(survey.id) }); - jest.spyOn(engagementService, 'getEngagement').mockReturnValue(Promise.resolve(draftEngagement)); - jest.spyOn(engagementSlugService, 'getSlugByEngagementId').mockReturnValue(Promise.resolve(engagementSlug)); - jest.spyOn(surveyService, 'getSurvey').mockReturnValue(Promise.resolve(survey)); - const fetchSurveyReportSettingsMock = jest - .spyOn(reportSettingsService, 'fetchSurveyReportSettings') - .mockReturnValue(Promise.resolve(SurveyReportSettings)); const updateSurveyReportSettingsMock = jest .spyOn(reportSettingsService, 'updateSurveyReportSettings') .mockReturnValue(Promise.resolve(SurveyReportSettings)); @@ -78,32 +97,32 @@ describe('Survey report settings tests', () => { }); test('View survey report settings page', async () => { - render(); - - await waitFor(() => { - expect(fetchSurveyReportSettingsMock).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(screen.getByText(surveyReportSettingOne.question_type)).toBeVisible(); - expect(screen.getByText(surveyReportSettingOne.question)).toBeVisible(); - expect(screen.getByText(surveyReportSettingTwo.question_type)).toBeVisible(); - expect(screen.getByText(surveyReportSettingTwo.question)).toBeVisible(); - }); + const { container } = render(); + + await waitFor( + () => { + expect(screen.getByText(surveyReportSettingOne.question_type)).toBeVisible(); + expect(screen.getByText(surveyReportSettingOne.question)).toBeVisible(); + expect(screen.getByText(surveyReportSettingTwo.question_type)).toBeVisible(); + expect(screen.getByText(surveyReportSettingTwo.question)).toBeVisible(); + }, + { timeout: 9000 }, + ); expect(screen.getByTestId(`checkbox-${surveyReportSettingOne.id}`).children[0]).toBeChecked(); expect(screen.getByTestId(`checkbox-${surveyReportSettingTwo.id}`).children[0]).not.toBeChecked(); }); test('Search question by question text', async () => { - const { container } = render(); + const { container } = render(); - await waitFor(() => { - expect(fetchSurveyReportSettingsMock).toHaveBeenCalledTimes(1); - }); - - await waitFor(() => { - expect(screen.getByText(surveyReportSettingOne.question_type)).toBeVisible(); - }); + await waitFor( + () => { + expect(screen.getByText(surveyReportSettingOne.question)).toBeVisible(); + expect(screen.getByText(surveyReportSettingOne.question_type)).toBeVisible(); + }, + { timeout: 9000 }, + ); const searchField = container.querySelector('input[name="searchText"]'); assert(searchField, 'Unable to find search field that matches the given query'); @@ -111,9 +130,10 @@ describe('Survey report settings tests', () => { fireEvent.change(searchField, { target: { value: surveyReportSettingOne.question } }); fireEvent.click(screen.getByTestId('survey/report/search-button')); - const table = screen.getByRole('table'); - const tableBody = table.querySelector('tbody'); - assert(searchField, 'Unable to find table'); + screen.debug(undefined, 99999999); + const table = container.querySelector('table'); + const tableBody = container.querySelector('tbody'); + assert(table, 'Unable to find table'); // plus one for the row that displays a loader when the table is loading const originalNumberOfRows = SurveyReportSettings.length + 1; @@ -124,13 +144,15 @@ describe('Survey report settings tests', () => { }); test('Survey report settings can be updated', async () => { - render(); - - await waitFor(() => { - expect(fetchSurveyReportSettingsMock).toHaveBeenCalledTimes(1); - expect(screen.getByText(surveyReportSettingOne.question)).toBeVisible(); - expect(screen.getByText(surveyReportSettingTwo.question)).toBeVisible(); - }); + render(); + + await waitFor( + () => { + expect(screen.getByText(surveyReportSettingOne.question)).toBeVisible(); + expect(screen.getByText(surveyReportSettingTwo.question)).toBeVisible(); + }, + { timeout: 9000 }, + ); const uncheckedBox = screen.getByTestId(`checkbox-${surveyReportSettingTwo.id}`).children[0]; expect(uncheckedBox).toBeInTheDocument(); From e02290c4c177342ffb6ad8933a5c2cfd9e73c7bd Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 15:18:22 -0700 Subject: [PATCH 3/6] Address Sonarcloud issues --- .../src/components/survey/submit/PreviewBanner.tsx | 11 ++++++----- met-web/src/components/survey/submit/SurveyForm.tsx | 3 +-- .../components/survey/SurveyReportSettings.test.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/met-web/src/components/survey/submit/PreviewBanner.tsx b/met-web/src/components/survey/submit/PreviewBanner.tsx index 3c9700da4..df9e2dc0d 100644 --- a/met-web/src/components/survey/submit/PreviewBanner.tsx +++ b/met-web/src/components/survey/submit/PreviewBanner.tsx @@ -8,16 +8,13 @@ import { Header1 } from 'components/common/Typography'; import { Button } from 'components/common/Input'; import { Survey } from 'models/survey'; -export const PreviewBanner = () => { +const Banner = (survey: Survey) => { const navigate = useNavigate(); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); - const { survey } = useRouteLoaderData('survey') as { survey: Promise }; - if (!isLoggedIn || !survey) { return null; } - - const Banner = (survey: Survey) => ( + return ( @@ -35,6 +32,10 @@ export const PreviewBanner = () => { ); +}; + +export const PreviewBanner = () => { + const { survey } = useRouteLoaderData('survey') as { survey: Promise }; return ( }> diff --git a/met-web/src/components/survey/submit/SurveyForm.tsx b/met-web/src/components/survey/submit/SurveyForm.tsx index a3eb2ceef..f8c7af995 100644 --- a/met-web/src/components/survey/submit/SurveyForm.tsx +++ b/met-web/src/components/survey/submit/SurveyForm.tsx @@ -2,9 +2,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { Grid, Stack } from '@mui/material'; import FormSubmit from 'components/Form/FormSubmit'; import { FormSubmissionData } from 'components/Form/types'; -import { useAppDispatch, useAppSelector } from 'hooks'; +import { useAppDispatch, useAppSelector, useAppTranslation } from 'hooks'; import { When } from 'react-if'; -import { useAppTranslation } from 'hooks'; import { submitSurvey } from 'services/submissionService'; import { useAsyncValue, useBlocker, useNavigate } from 'react-router-dom'; import { EmailVerification } from 'models/emailVerification'; diff --git a/met-web/tests/unit/components/survey/SurveyReportSettings.test.tsx b/met-web/tests/unit/components/survey/SurveyReportSettings.test.tsx index 84e4e9f78..4b4de4499 100644 --- a/met-web/tests/unit/components/survey/SurveyReportSettings.test.tsx +++ b/met-web/tests/unit/components/survey/SurveyReportSettings.test.tsx @@ -97,7 +97,7 @@ describe('Survey report settings tests', () => { }); test('View survey report settings page', async () => { - const { container } = render(); + render(); await waitFor( () => { From 29f32cee6951d37464d39c54091564d4e63529cf Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 16:40:33 -0700 Subject: [PATCH 4/6] Fix engagement loader? --- met-web/src/App.tsx | 1 + .../survey/building/SurveyLoader.tsx | 20 +++++++++++-------- met-web/src/routes/UnauthenticatedRoutes.tsx | 7 +------ .../emailVerificationService/index.ts | 7 ++++--- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/met-web/src/App.tsx b/met-web/src/App.tsx index 8184f3a3c..39532ac3b 100644 --- a/met-web/src/App.tsx +++ b/met-web/src/App.tsx @@ -206,6 +206,7 @@ const App = () => { [ { element: , + errorElement: , children: createRoutesFromElements(UnauthenticatedRoutes()), }, ], diff --git a/met-web/src/components/survey/building/SurveyLoader.tsx b/met-web/src/components/survey/building/SurveyLoader.tsx index caec70654..aea34e6d4 100644 --- a/met-web/src/components/survey/building/SurveyLoader.tsx +++ b/met-web/src/components/survey/building/SurveyLoader.tsx @@ -1,22 +1,26 @@ import { Params, defer } from 'react-router-dom'; import { getEmailVerification } from 'services/emailVerificationService'; import { getEngagement } from 'services/engagementService'; -import { getSlugByEngagementId } from 'services/engagementSlugService'; +import { getEngagementIdBySlug, getSlugByEngagementId } from 'services/engagementSlugService'; import { getSubmissionByToken } from 'services/submissionService'; import { getSurvey } from 'services/surveyService'; import { fetchSurveyReportSettings } from 'services/surveyService/reportSettingsService'; export const SurveyLoader = async ({ params }: { params: Params }) => { - const { surveyId, token, language } = params; - if (isNaN(Number(surveyId)) && !token) throw new Error('Invalid survey ID'); + const { surveyId, token, language, engagementId, slug: urlSlug } = params; + if (isNaN(Number(surveyId)) && !engagementId && !urlSlug) throw new Error('Invalid survey ID'); const verification = getEmailVerification(token ?? '').catch(() => null); const survey = surveyId ? getSurvey(Number(surveyId)) - : verification.then((response) => { - if (!response) throw new Error('Invalid token'); - return getSurvey(response.survey_id); - }); - const submission = verification.then((response) => getSubmissionByToken(response?.verification_token ?? '')); + : engagementId + ? getEngagement(Number(engagementId)).then((response) => Promise.resolve(response.surveys[0])) + : getEngagementIdBySlug(urlSlug ?? '').then((response) => + getEngagement(response.engagement_id).then((response) => Promise.resolve(response.surveys[0])), + ); + + const submission = verification.then( + (response) => response && getSubmissionByToken(response?.verification_token ?? ''), + ); const reportSettings = survey.then((response) => fetchSurveyReportSettings(response.id.toString())); const engagement = survey.then((response) => { if (!response.engagement_id) return null; diff --git a/met-web/src/routes/UnauthenticatedRoutes.tsx b/met-web/src/routes/UnauthenticatedRoutes.tsx index 5f9128799..780e00686 100644 --- a/met-web/src/routes/UnauthenticatedRoutes.tsx +++ b/met-web/src/routes/UnauthenticatedRoutes.tsx @@ -36,12 +36,7 @@ const UnauthenticatedRoutes = () => { /> } /> - } - element={} - /> + } /> } /> diff --git a/met-web/src/services/emailVerificationService/index.ts b/met-web/src/services/emailVerificationService/index.ts index 12b4babda..44faaf2fb 100644 --- a/met-web/src/services/emailVerificationService/index.ts +++ b/met-web/src/services/emailVerificationService/index.ts @@ -20,11 +20,12 @@ export const verifyEmailVerification = async (token: string): Promise(url); - if (response.data) { + try { + const response = await http.PutRequest(url); return response.data; + } catch (err) { + return Promise.reject(err); } - return Promise.reject('Failed to fetch email verification'); }; interface CreateEmailVerification { From dae1a100fc4fc1e7fa9bf5f6f5aa3d91aaa1bda8 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 16:53:16 -0700 Subject: [PATCH 5/6] Expand ternary expression --- .../survey/building/SurveyLoader.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/met-web/src/components/survey/building/SurveyLoader.tsx b/met-web/src/components/survey/building/SurveyLoader.tsx index aea34e6d4..332b171a5 100644 --- a/met-web/src/components/survey/building/SurveyLoader.tsx +++ b/met-web/src/components/survey/building/SurveyLoader.tsx @@ -8,15 +8,17 @@ import { fetchSurveyReportSettings } from 'services/surveyService/reportSettings export const SurveyLoader = async ({ params }: { params: Params }) => { const { surveyId, token, language, engagementId, slug: urlSlug } = params; - if (isNaN(Number(surveyId)) && !engagementId && !urlSlug) throw new Error('Invalid survey ID'); + if (isNaN(Number(surveyId)) && !isNaN(Number(engagementId)) && !urlSlug) throw new Error('Invalid survey ID'); const verification = getEmailVerification(token ?? '').catch(() => null); - const survey = surveyId - ? getSurvey(Number(surveyId)) - : engagementId - ? getEngagement(Number(engagementId)).then((response) => Promise.resolve(response.surveys[0])) - : getEngagementIdBySlug(urlSlug ?? '').then((response) => - getEngagement(response.engagement_id).then((response) => Promise.resolve(response.surveys[0])), - ); + const getSurveyPromise = () => { + if (!isNaN(Number(surveyId))) return getSurvey(Number(surveyId)); + if (urlSlug) + return getEngagementIdBySlug(urlSlug ?? '').then((response) => + getEngagement(response.engagement_id).then((response) => Promise.resolve(response.surveys[0])), + ); + return getEngagement(Number(engagementId)).then((response) => Promise.resolve(response.surveys[0])); + }; + const survey = getSurveyPromise(); const submission = verification.then( (response) => response && getSubmissionByToken(response?.verification_token ?? ''), From ebcde44b0f4af467f6f987c352f1872371b3d878 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 9 Jul 2024 16:59:05 -0700 Subject: [PATCH 6/6] Restructure message a bit --- met-web/src/components/survey/submit/SurveyForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-web/src/components/survey/submit/SurveyForm.tsx b/met-web/src/components/survey/submit/SurveyForm.tsx index f8c7af995..aafebfc52 100644 --- a/met-web/src/components/survey/submit/SurveyForm.tsx +++ b/met-web/src/components/survey/submit/SurveyForm.tsx @@ -59,7 +59,7 @@ export const SurveyForm = () => { data: { survey_id: survey.id, engagement_id: survey.engagement_id }, }); } catch (error) { - console.log('Survey submit notification snowplow error:', error); + console.log('Error while firing snowplow event for survey submission:', error); } dispatch( openNotification({