From 5db537ab17084fe36dba39f6dc261ce487d99d22 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 24 Jan 2024 04:24:29 +0000 Subject: [PATCH 01/19] Consolidate additional response options into single type --- src/shared/api/services/axios.ts | 2 ++ src/shared/api/types/item.ts | 55 +++++++++----------------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/shared/api/services/axios.ts b/src/shared/api/services/axios.ts index b34aaac03..f7c9f50a3 100644 --- a/src/shared/api/services/axios.ts +++ b/src/shared/api/services/axios.ts @@ -19,6 +19,8 @@ axiosService.interceptors.request.use( const tokens = secureTokensStorage.getTokens() if (tokens?.accessToken && tokens?.tokenType) { + // @ts-expect-error This error was introduced in https://github.com/ChildMindInstitute/mindlogger-web-refactor/pull/342 + // and I don't really want to change it as part of this PR config.headers.Authorization = `${tokens.tokenType} ${tokens.accessToken}` } diff --git a/src/shared/api/types/item.ts b/src/shared/api/types/item.ts index c2b5dc83d..3c70f36af 100644 --- a/src/shared/api/types/item.ts +++ b/src/shared/api/types/item.ts @@ -54,6 +54,13 @@ export type ResponseValuesDTO = export type EmptyResponseValuesDTO = null +export type AdditionalResponseOptionConfigDTO = { + additionalResponseOption: { + textInputOption: boolean + textInputRequired: boolean + } +} + export interface TextItemDTO extends ItemDetailsBaseDTO { responseType: "text" config: TextItemConfigDTO @@ -77,7 +84,7 @@ export interface CheckboxItemDTO extends ItemDetailsBaseDTO { responseValues: CheckboxItemResponseValuesDTO } -export type CheckboxItemConfigDTO = { +export type CheckboxItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean randomizeOptions: boolean @@ -86,10 +93,6 @@ export type CheckboxItemConfigDTO = { setAlerts: boolean addTooltip: boolean setPalette: boolean - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export type CheckboxItemResponseValuesDTO = { @@ -112,7 +115,7 @@ export interface RadioItemDTO extends ItemDetailsBaseDTO { responseValues: RadioItemResponseValuesDTO } -export type RadioItemConfigDTO = { +export type RadioItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean randomizeOptions: boolean @@ -122,10 +125,6 @@ export type RadioItemConfigDTO = { addTooltip: boolean setPalette: boolean autoAdvance: boolean - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export type RadioItemResponseValuesDTO = { @@ -148,7 +147,7 @@ export interface SliderItemDTO extends ItemDetailsBaseDTO { responseValues: SliderItemResponseValuesDTO } -export type SliderItemConfigDTO = { +export type SliderItemConfigDTO = AdditionalResponseOptionConfigDTO & { addScores: boolean setAlerts: boolean showTickMarks: boolean @@ -157,10 +156,6 @@ export type SliderItemConfigDTO = { removeBackButton: boolean skippableItem: boolean timer: number | null - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export type SliderItemResponseValuesDTO = { @@ -185,13 +180,9 @@ export interface SelectorItemDTO extends ItemDetailsBaseDTO { responseValues: SelectorItemResponseValues } -export type SelectorItemConfigDTO = { +export type SelectorItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export type SelectorItemResponseValues = { @@ -216,14 +207,10 @@ export interface DateItemDTO extends ItemDetailsBaseDTO { responseValues: EmptyResponseValuesDTO } -export type DateItemConfigDTO = { +export type DateItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean timer: number | null - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export interface TimeItemDTO extends ItemDetailsBaseDTO { @@ -232,14 +219,10 @@ export interface TimeItemDTO extends ItemDetailsBaseDTO { responseValues: EmptyResponseValuesDTO } -export type TimeItemConfigDTO = { +export type TimeItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean timer: number | null - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export interface TimeRangeItemDTO extends ItemDetailsBaseDTO { @@ -248,14 +231,10 @@ export interface TimeRangeItemDTO extends ItemDetailsBaseDTO { responseValues: EmptyResponseValuesDTO } -export type TimeRangeItemConfigDTO = { +export type TimeRangeItemConfigDTO = AdditionalResponseOptionConfigDTO & { removeBackButton: boolean skippableItem: boolean timer: number | null - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export interface AudioPlayerItemDTO extends ItemDetailsBaseDTO { @@ -264,14 +243,10 @@ export interface AudioPlayerItemDTO extends ItemDetailsBaseDTO { responseValues: AudioPlayerItemResponseValuesDTO } -export type AudioPlayerItemConfigDTO = { +export type AudioPlayerItemConfigDTO = AdditionalResponseOptionConfigDTO & { playOnce: boolean removeBackButton: boolean skippableItem: boolean - additionalResponseOption: { - textInputOption: boolean - textInputRequired: boolean - } } export type AudioPlayerItemResponseValuesDTO = { From 99919be255f4b6edaa1b52fda88daa3ee0c6735d Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 24 Jan 2024 21:43:16 +0000 Subject: [PATCH 02/19] Add additional_text translation The French translation is missing for now. I'll replace it once I'm familiar with how to do the translation --- src/i18n/en/translation.json | 3 ++- src/i18n/fr/translation.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 24cfa0b6a..ed4f419f0 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -256,7 +256,8 @@ "footer_text": "CHILD MIND INSTITUTE, INC. AND CHILD MIND MEDICAL PRACTICE, PLLC (TOGETHER, “CMI”) DOES NOT DIRECTLY OR INDIRECTLY PRACTICE MEDICINE OR DISPENSE MEDICAL ADVICE AS PART OF THIS QUESTIONNAIRE. CMI ASSUMES NO LIABILITY FOR ANY DIAGNOSIS, TREATMENT, DECISION MADE, OR ACTION TAKEN IN RELIANCE UPON INFORMATION PROVIDED BY THIS QUESTIONNAIRE, AND ASSUMES NO RESPONSIBILITY FOR YOUR USE OF THIS QUESTIONNAIRE.", "child_score": "Your Child's Score", "child_score_on_subscale": "Your child’s score on the {{name}} subscale was", - "fill_out_fields": "Please fill out the required fields" + "fill_out_fields": "Please fill out the required fields", + "additional_text": "Additional Text" }, "no_markdown": "The authors of this applet have not provided any information!", "no_applets": "You currently do not have any applets." diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index 3eeedc850..e55434293 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -264,7 +264,8 @@ "child_score_on_subscale": "Le score de votre enfant pour la sous-échelle {{name}} est de", "share_report": "Partager le rapport", "share_all_reports": "Partager tous les rapports", - "fill_out_fields": "Veuillez remplir les champs obligatoires" + "fill_out_fields": "Veuillez remplir les champs obligatoires", + "additional_text": "Additional Text" }, "no_markdown": "Les auteurs de cette applet n'ont fourni aucune information !", "no_applets": "Vous n'avez actuellement pas d'applets." From 9898fdf862d128b12d2e53a8869caf9e9b67afcb Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 24 Jan 2024 21:52:48 +0000 Subject: [PATCH 03/19] Prevent auto advance if the question has additional text --- src/entities/activity/lib/helpers.ts | 34 +++++++++++++++++++ src/entities/applet/model/types.ts | 5 +++ .../model/hooks/useAutoForward.ts | 14 +++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/entities/activity/lib/helpers.ts b/src/entities/activity/lib/helpers.ts index 7b4559162..0efb88f03 100644 --- a/src/entities/activity/lib/helpers.ts +++ b/src/entities/activity/lib/helpers.ts @@ -1,4 +1,5 @@ import { supportableResponseTypes } from "~/abstract/lib/constants" +import { appletModel } from "~/entities/applet" import { ItemResponseTypeDTO } from "~/shared/api" export function isSupportedActivity(itemResponseTypes?: Array): boolean { @@ -8,3 +9,36 @@ export function isSupportedActivity(itemResponseTypes?: Array supportableResponseTypes.includes(type)) } + +/** + * Check whether an item supports an additional response field + * @param item Any item + * @returns Whether the item type supports additional text responses + */ +export const supportsAdditonalResponseField = ( + item: appletModel.ItemRecord, +): item is appletModel.ItemWithAdditionalResponse => { + return [ + "singleSelect", + "multiSelect", + "slider", + "date", + "numberSelect", + "time", + "timeRange", + "drawing", + "photo", + "video", + "geolocation", + "audio", + ].includes(item.responseType) +} + +/** + * Check whether an item has been configured with an additional response field + * @param item Any item, even those that don't support additional response fields + * @returns Whether the item has an additional response field + */ +export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => { + return supportsAdditonalResponseField(item) && item.config.additionalResponseOption.textInputOption +} diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index fd1852536..a93e01876 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -41,6 +41,11 @@ export type ItemRecord = | TimeRangeItem | AudioPlayerItem +export type ItemWithAdditionalResponse = Extract< + ItemRecord, + CheckboxItem | RadioItem | SliderItem | SelectorItem | DateItem | TimeItem | TimeRangeItem | AudioPlayerItem +> + export type ActivityProgress = { items: ItemRecord[] step: number diff --git a/src/widgets/ActivityDetails/model/hooks/useAutoForward.ts b/src/widgets/ActivityDetails/model/hooks/useAutoForward.ts index 1f0daa9c7..0c81811e5 100644 --- a/src/widgets/ActivityDetails/model/hooks/useAutoForward.ts +++ b/src/widgets/ActivityDetails/model/hooks/useAutoForward.ts @@ -1,5 +1,6 @@ import { useEffect } from "react" +import { hasAdditionalResponse } from "~/entities/activity/lib/helpers" import { appletModel } from "~/entities/applet" import { usePrevious } from "~/shared/utils" @@ -31,7 +32,18 @@ export const useAutoForward = ({ item, onForward, hasNextStep }: Props) => { const isHasAnswer = item.answer.length > 0 - if (isSingleSelect && isHasAnswer && hasNextStep && isAnswerChanged && isAutoForwardEnabled) { + // If there's an additional text field we probably shouldn't auto advance, + // even if the field is populated + const hasAdditionalTextField = hasAdditionalResponse(item) + + if ( + isSingleSelect && + isHasAnswer && + hasNextStep && + isAnswerChanged && + isAutoForwardEnabled && + !hasAdditionalTextField + ) { onForward() } }, [hasNextStep, item, onForward, prevItem?.answer, prevItem?.id]) From 80d91b3a8ca11468e94fc2982e1618485e171c4e Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Thu, 25 Jan 2024 15:24:29 +0000 Subject: [PATCH 04/19] Add additional_text translation --- src/i18n/fr/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index e55434293..3357365b0 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -265,7 +265,7 @@ "share_report": "Partager le rapport", "share_all_reports": "Partager tous les rapports", "fill_out_fields": "Veuillez remplir les champs obligatoires", - "additional_text": "Additional Text" + "additional_text": "Texte supplémentaire" }, "no_markdown": "Les auteurs de cette applet n'ont fourni aucune information !", "no_applets": "Vous n'avez actuellement pas d'applets." From 07159bc35e6e9fe38fd6652ea58bc76a292ca977 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Mon, 29 Jan 2024 21:19:35 -0500 Subject: [PATCH 05/19] Create additional text response field --- .../activity/ui/AdditionalTextResponse.tsx | 26 +++++++++++++++++++ src/entities/activity/ui/index.ts | 1 + 2 files changed, 27 insertions(+) create mode 100644 src/entities/activity/ui/AdditionalTextResponse.tsx diff --git a/src/entities/activity/ui/AdditionalTextResponse.tsx b/src/entities/activity/ui/AdditionalTextResponse.tsx new file mode 100644 index 000000000..1069ca0b9 --- /dev/null +++ b/src/entities/activity/ui/AdditionalTextResponse.tsx @@ -0,0 +1,26 @@ +import TextField from "@mui/material/TextField" + +type AdditionalTextResponseProps = { + value: string + onValueChange: (value: string) => void +} + +export const AdditionalTextResponse = ({ value, onValueChange }: AdditionalTextResponseProps) => { + const onHandleValueChange = (value: string) => { + if (value.length === 0) { + return onValueChange("") + } + + return onValueChange(value) + } + + return ( + onHandleValueChange(e.target.value)} + disabled={false} + /> + ) +} diff --git a/src/entities/activity/ui/index.ts b/src/entities/activity/ui/index.ts index b9f4c2f96..54d1c2fb3 100644 --- a/src/entities/activity/ui/index.ts +++ b/src/entities/activity/ui/index.ts @@ -1,2 +1,3 @@ export * from "./ActivityCardItem" export * from "./ItemCardButtons" +export * from "./AdditionalTextResponse" From d9f5be77ded46e2b31f64d3d4f03e7771aca9f25 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Mon, 29 Jan 2024 21:20:56 -0500 Subject: [PATCH 06/19] Create reducer for saving additional text --- src/entities/applet/model/slice.ts | 25 +++++++++++++++++++++++++ src/entities/applet/model/types.ts | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/src/entities/applet/model/slice.ts b/src/entities/applet/model/slice.ts index 671d61de4..2c1470e64 100644 --- a/src/entities/applet/model/slice.ts +++ b/src/entities/applet/model/slice.ts @@ -15,6 +15,7 @@ import { UpdateStepPayload, UpdateUserEventByIndexPayload, SaveGroupProgressPayload, + SaveItemAdditionalTextPayload, } from "./types" import { ActivityPipelineType, GroupProgress, FlowProgress, getProgressId, GroupProgressState } from "~/abstract/lib" @@ -82,6 +83,30 @@ const appletsSlice = createSlice({ activityProgress.items[itemIndex].answer = action.payload.answer }, + + /** + * Reducer for saving additionaltext + * @param state + * @param action + * @returns + */ + saveAdditionalText: (state, action: PayloadAction) => { + const id = getProgressId(action.payload.entityId, action.payload.eventId) + const activityProgress = state.progress[id] + + if (!activityProgress) { + return state + } + + const itemIndex = activityProgress.items.findIndex(({ id }) => id === action.payload.itemId) + + if (itemIndex === -1) { + return state + } + + activityProgress.items[itemIndex].additionalText = action.payload.additionalText + }, + saveUserEvent: (state, action: PayloadAction) => { const id = getProgressId(action.payload.entityId, action.payload.eventId) const activityProgress = state.progress[id] diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index a93e01876..8d3664039 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -82,6 +82,13 @@ export type SaveItemAnswerPayload = { answer: string[] } +export type SaveItemAdditionalTextPayload = { + entityId: string + eventId: string + itemId: string + additionalText: string +} + export type UpdateStepPayload = { activityId: string eventId: string From 5ada25d55595a91b3b42570669a058a4e6aa5e87 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Mon, 29 Jan 2024 21:22:38 -0500 Subject: [PATCH 07/19] Create hook for saving additional text --- .../model/hooks/useSaveActivityItemAnswer.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts b/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts index b2eaa4be0..ee60ba425 100644 --- a/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts +++ b/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts @@ -26,7 +26,22 @@ export const useSaveItemAnswer = ({ activityId, eventId }: Props) => { [dispatch, activityId, eventId], ) + const saveItemAdditionalText = useCallback( + (itemId: string, additionalText: string) => { + dispatch( + actions.saveAdditionalText({ + entityId: activityId, + eventId, + itemId, + additionalText, + }), + ) + }, + [dispatch, activityId, eventId], + ) + return { saveItemAnswer, + saveItemAdditionalText, } } From 754f322577eeefbb0bbd16f601d219a6fa29e15b Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Mon, 29 Jan 2024 21:24:01 -0500 Subject: [PATCH 08/19] Create additional text field in UI --- src/entities/activity/lib/types/item.ts | 1 + src/entities/activity/ui/ActivityCardItem.tsx | 16 ++++++++++++++++ .../ui/AssessmentPassingScreen.tsx | 8 +++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/entities/activity/lib/types/item.ts b/src/entities/activity/lib/types/item.ts index 62af46982..ccac82496 100644 --- a/src/entities/activity/lib/types/item.ts +++ b/src/entities/activity/lib/types/item.ts @@ -48,6 +48,7 @@ export interface ActivityItemBase { config: Config responseValues: ResponseValues answer: Answer + additionalText?: string | null conditionalLogic: ConditionalLogic | null } diff --git a/src/entities/activity/ui/ActivityCardItem.tsx b/src/entities/activity/ui/ActivityCardItem.tsx index 5e1f71166..39fef8e2a 100644 --- a/src/entities/activity/ui/ActivityCardItem.tsx +++ b/src/entities/activity/ui/ActivityCardItem.tsx @@ -1,10 +1,13 @@ import { useMemo } from "react" +import { AdditionalTextResponse } from "./AdditionalTextResponse" import { ItemPicker } from "./items/ItemPicker" +import { hasAdditionalResponse } from "../lib" import { appletModel } from "~/entities/applet" import { SliderAnimation } from "~/shared/animations" import { CardItem } from "~/shared/ui" +import { useCustomTranslation } from "~/shared/utils" type ActivityCardItemProps = { item: appletModel.ItemRecord @@ -13,6 +16,8 @@ type ActivityCardItemProps = { onValueChange: (value: string[]) => void + onItemAdditionalTextChange: (value: string) => void + replaceText: (value: string) => string step: number @@ -27,6 +32,7 @@ export const ActivityCardItem = ({ step, prevStep, onValueChange, + onItemAdditionalTextChange, }: ActivityCardItemProps) => { const questionText = useMemo(() => { return replaceText(item.question) @@ -34,6 +40,8 @@ export const ActivityCardItem = ({ const isOptionalFlagHidden = ["message", "audioPlayer", "splashScreen"].includes(item.responseType) + const { t } = useCustomTranslation() + return ( + {hasAdditionalResponse(item) && ( + + + + )} ) } diff --git a/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx b/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx index 147079a11..2154ded12 100644 --- a/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx +++ b/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx @@ -55,7 +55,7 @@ export const AssessmentPassingScreen = (props: Props) => { eventId, }) - const { saveItemAnswer } = appletModel.hooks.useSaveItemAnswer({ + const { saveItemAnswer, saveItemAdditionalText } = appletModel.hooks.useSaveItemAnswer({ activityId, eventId, }) @@ -159,6 +159,11 @@ export const AssessmentPassingScreen = (props: Props) => { }) } + const onItemAdditionalTextChange = (value: string) => { + saveItemAdditionalText(item.id, value) + // TODO: saveSetAdditionalTextUserEvent + } + useAutoForward({ item, hasNextStep, @@ -196,6 +201,7 @@ export const AssessmentPassingScreen = (props: Props) => { step={step} prevStep={prevStep} onValueChange={onItemValueChange} + onItemAdditionalTextChange={onItemAdditionalTextChange} /> )} From ebe4e96cf5636163b46716d1bc0d02983d0f9205 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Mon, 29 Jan 2024 21:24:13 -0500 Subject: [PATCH 09/19] Update mapper functions to account for additional text --- src/widgets/ActivityDetails/model/mappers.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/widgets/ActivityDetails/model/mappers.ts b/src/widgets/ActivityDetails/model/mappers.ts index 79aab39e6..a4ea41b14 100644 --- a/src/widgets/ActivityDetails/model/mappers.ts +++ b/src/widgets/ActivityDetails/model/mappers.ts @@ -95,7 +95,7 @@ function convertToSingleSelectAnswer(item: RadioItem): ItemAnswer Number(strValue)), - text: null, + text: item.additionalText || null, }, itemId: item.id, } @@ -129,7 +129,7 @@ function convertToSliderAnswer(item: SliderItem): ItemAnswer { return { answer: { value: dateToDayMonthYear(new Date(item.answer[0])), - text: null, + text: item.additionalText || null, }, itemId: item.id, } @@ -187,7 +187,7 @@ function convertToTimeAnswer(item: TimeItem): ItemAnswer { return { answer: { value: dateToHourMinute(new Date(item.answer[0])), - text: null, + text: item.additionalText || null, }, itemId: item.id, } @@ -210,7 +210,7 @@ function convertToTimeRangeAnswer(item: TimeRangeItem): ItemAnswer Date: Mon, 29 Jan 2024 21:49:05 -0500 Subject: [PATCH 10/19] Update answer validation to account for additional text If an item requires additional text, the user should be blocked from proceeding if they don't provide it --- src/entities/activity/lib/helpers.ts | 9 +++++++++ src/entities/activity/ui/ActivityCardItem.tsx | 8 ++++++-- src/i18n/en/translation.json | 4 +++- src/i18n/fr/translation.json | 4 +++- src/widgets/ActivityDetails/model/validateItem.ts | 10 ++++++++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/entities/activity/lib/helpers.ts b/src/entities/activity/lib/helpers.ts index 0efb88f03..9ef4d1381 100644 --- a/src/entities/activity/lib/helpers.ts +++ b/src/entities/activity/lib/helpers.ts @@ -42,3 +42,12 @@ export const supportsAdditonalResponseField = ( export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => { return supportsAdditonalResponseField(item) && item.config.additionalResponseOption.textInputOption } + +/** + * Check whether an item has been configured with a required additional response field + * @param item Any item, even those that don't support additional response fields + * @returns Whether the item requires an additional response + */ +export const requiresAdditionalResponse = (item: appletModel.ItemRecord): boolean => { + return supportsAdditonalResponseField(item) && item.config.additionalResponseOption.textInputRequired +} diff --git a/src/entities/activity/ui/ActivityCardItem.tsx b/src/entities/activity/ui/ActivityCardItem.tsx index 39fef8e2a..6036e6afb 100644 --- a/src/entities/activity/ui/ActivityCardItem.tsx +++ b/src/entities/activity/ui/ActivityCardItem.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react" import { AdditionalTextResponse } from "./AdditionalTextResponse" import { ItemPicker } from "./items/ItemPicker" -import { hasAdditionalResponse } from "../lib" +import { hasAdditionalResponse, requiresAdditionalResponse } from "../lib" import { appletModel } from "~/entities/applet" import { SliderAnimation } from "~/shared/animations" @@ -42,6 +42,10 @@ export const ActivityCardItem = ({ const { t } = useCustomTranslation() + const additionalTextLabel = requiresAdditionalResponse(item) + ? t("additional.additional_text") + : t("additional.additional_text_required") + return ( {hasAdditionalResponse(item) && ( diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index ed4f419f0..489faf9e8 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -15,6 +15,7 @@ "save_and_exit": "Save & exit", "optional": "Optional", "pleaseAnswerTheQuestion": "Please answer the question.", + "pleaseProvideAdditionalText": "Please provide additional text", "onlyNumbersAllowed": "Only numbers are allowed", "question_count_plural": "{{length}} questions", @@ -257,7 +258,8 @@ "child_score": "Your Child's Score", "child_score_on_subscale": "Your child’s score on the {{name}} subscale was", "fill_out_fields": "Please fill out the required fields", - "additional_text": "Additional Text" + "additional_text": "Additional Text", + "additional_text_required": "Additional Text (required)" }, "no_markdown": "The authors of this applet have not provided any information!", "no_applets": "You currently do not have any applets." diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index 3357365b0..d5196fb29 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -15,6 +15,7 @@ "save_and_exit": "Enregistrer et quitter", "optional": "Facultatif", "pleaseAnswerTheQuestion": "Veuillez répondre à la question.", + "pleaseProvideAdditionalText": "Veuillez fournir un texte supplémentaire", "onlyNumbersAllowed": "Seuls les numéros sont autorisés", @@ -265,7 +266,8 @@ "share_report": "Partager le rapport", "share_all_reports": "Partager tous les rapports", "fill_out_fields": "Veuillez remplir les champs obligatoires", - "additional_text": "Texte supplémentaire" + "additional_text": "Texte supplémentaire", + "additional_text_required": "Texte supplémentaire (obligatoire)" }, "no_markdown": "Les auteurs de cette applet n'ont fourni aucune information !", "no_applets": "Vous n'avez actuellement pas d'applets." diff --git a/src/widgets/ActivityDetails/model/validateItem.ts b/src/widgets/ActivityDetails/model/validateItem.ts index 835e7fc3e..5879303e2 100644 --- a/src/widgets/ActivityDetails/model/validateItem.ts +++ b/src/widgets/ActivityDetails/model/validateItem.ts @@ -1,3 +1,4 @@ +import { hasAdditionalResponse, requiresAdditionalResponse } from "~/entities/activity/lib/helpers" import { appletModel } from "~/entities/applet" import { ActivityDTO } from "~/shared/api" import { stringContainsOnlyNumbers, validateDate, validateTime } from "~/shared/utils" @@ -98,5 +99,14 @@ export function validateBeforeMoveForward({ item, activity, showWarning }: Valid return false } + const hasAdditionalTextField = hasAdditionalResponse(item) + const isAdditionalTextRequired = hasAdditionalTextField && requiresAdditionalResponse(item) + const isAdditionalTextEmpty = hasAdditionalTextField && !item.additionalText + + if (isAdditionalTextRequired && isAdditionalTextEmpty) { + showWarning("pleaseProvideAdditionalText") + return false + } + return true } From 8987f06c43d4bc9b08f9576ec972692771bb9781 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Tue, 30 Jan 2024 05:22:29 +0000 Subject: [PATCH 11/19] Remove @ts-expect-error directive --- src/shared/api/services/axios.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/api/services/axios.ts b/src/shared/api/services/axios.ts index f7c9f50a3..b34aaac03 100644 --- a/src/shared/api/services/axios.ts +++ b/src/shared/api/services/axios.ts @@ -19,8 +19,6 @@ axiosService.interceptors.request.use( const tokens = secureTokensStorage.getTokens() if (tokens?.accessToken && tokens?.tokenType) { - // @ts-expect-error This error was introduced in https://github.com/ChildMindInstitute/mindlogger-web-refactor/pull/342 - // and I don't really want to change it as part of this PR config.headers.Authorization = `${tokens.tokenType} ${tokens.accessToken}` } From 750af831f11bc47040f804782cf34b652415bcff Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 31 Jan 2024 15:01:46 +0000 Subject: [PATCH 12/19] Replace conditional label with isOptional prop The CardItem component is already configured with a conditional label so the one I'm passing is redundant. Highlighting optionality is also more in line with the existing pattern --- src/entities/activity/ui/ActivityCardItem.tsx | 8 ++------ src/i18n/en/translation.json | 3 +-- src/i18n/fr/translation.json | 3 +-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/entities/activity/ui/ActivityCardItem.tsx b/src/entities/activity/ui/ActivityCardItem.tsx index 6036e6afb..b340ced8c 100644 --- a/src/entities/activity/ui/ActivityCardItem.tsx +++ b/src/entities/activity/ui/ActivityCardItem.tsx @@ -42,10 +42,6 @@ export const ActivityCardItem = ({ const { t } = useCustomTranslation() - const additionalTextLabel = requiresAdditionalResponse(item) - ? t("additional.additional_text") - : t("additional.additional_text_required") - return ( {hasAdditionalResponse(item) && ( + isOptional={!requiresAdditionalResponse(item)}> )} diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 489faf9e8..0994fa3c2 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -258,8 +258,7 @@ "child_score": "Your Child's Score", "child_score_on_subscale": "Your child’s score on the {{name}} subscale was", "fill_out_fields": "Please fill out the required fields", - "additional_text": "Additional Text", - "additional_text_required": "Additional Text (required)" + "additional_text": "Additional Text" }, "no_markdown": "The authors of this applet have not provided any information!", "no_applets": "You currently do not have any applets." diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index d5196fb29..cfc6095de 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -266,8 +266,7 @@ "share_report": "Partager le rapport", "share_all_reports": "Partager tous les rapports", "fill_out_fields": "Veuillez remplir les champs obligatoires", - "additional_text": "Texte supplémentaire", - "additional_text_required": "Texte supplémentaire (obligatoire)" + "additional_text": "Texte supplémentaire" }, "no_markdown": "Les auteurs de cette applet n'ont fourni aucune information !", "no_applets": "Vous n'avez actuellement pas d'applets." From dc9b6a811329c4863046d1b0b422ed502fb63886 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 31 Jan 2024 15:46:27 +0000 Subject: [PATCH 13/19] Save additional text user events --- .../applet/model/hooks/useUserEvents.ts | 49 ++++++++++++++++++- src/entities/applet/model/types.ts | 2 +- .../ui/AssessmentPassingScreen.tsx | 14 ++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/entities/applet/model/hooks/useUserEvents.ts b/src/entities/applet/model/hooks/useUserEvents.ts index 781434d62..7d75d0b6c 100644 --- a/src/entities/applet/model/hooks/useUserEvents.ts +++ b/src/entities/applet/model/hooks/useUserEvents.ts @@ -88,5 +88,52 @@ export const useUserEvents = (props: Props) => { [activityProgress, dispatch, props.activityId, props.eventId], ) - return { saveUserEventByType, saveSetAnswerUserEvent } + const saveSetAdditionalTextUserEvent = useCallback( + (item: ItemRecord) => { + if (!activityProgress || item.additionalText === null || item.additionalText === undefined) { + return + } + + const userEvents = activityProgress.userEvents + + const activityItemScreenId = getActivityItemScreenId(props.activityId, item.id) + + if (userEvents.length > 0) { + const lastUserEvent = userEvents[userEvents.length - 1] + + if (lastUserEvent.screen === activityItemScreenId && lastUserEvent.type === "SET_ADDITIONAL_TEXT") { + return dispatch( + actions.updateUserEventByIndex({ + entityId: props.activityId, + eventId: props.eventId, + userEventIndex: userEvents.length - 1, + userEvent: { + type: "SET_ADDITIONAL_TEXT", + screen: activityItemScreenId, + time: Date.now(), + response: item.additionalText, + }, + }), + ) + } + } + + return dispatch( + actions.saveUserEvent({ + entityId: props.activityId, + eventId: props.eventId, + itemId: item.id, + userEvent: { + type: "SET_ADDITIONAL_TEXT", + screen: activityItemScreenId, + time: Date.now(), + response: item.additionalText, + }, + }), + ) + }, + [activityProgress, dispatch, props.activityId, props.eventId], + ) + + return { saveUserEventByType, saveSetAnswerUserEvent, saveSetAdditionalTextUserEvent } } diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index 8d3664039..f949a7c7e 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -13,7 +13,7 @@ import { TimeRangeItem, } from "~/entities/activity/lib" -export type UserEventTypes = "SET_ANSWER" | "PREV" | "NEXT" | "SKIP" | "DONE" +export type UserEventTypes = "SET_ANSWER" | "SET_ADDITIONAL_TEXT" | "PREV" | "NEXT" | "SKIP" | "DONE" export type UserEventResponse = | string diff --git a/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx b/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx index 2154ded12..1b4ed3afe 100644 --- a/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx +++ b/src/widgets/ActivityDetails/ui/AssessmentPassingScreen.tsx @@ -50,10 +50,11 @@ export const AssessmentPassingScreen = (props: Props) => { const { incrementStep, decrementStep } = appletModel.hooks.useActivityProgress() - const { saveUserEventByType, saveSetAnswerUserEvent } = appletModel.hooks.useUserEvents({ - activityId, - eventId, - }) + const { saveUserEventByType, saveSetAnswerUserEvent, saveSetAdditionalTextUserEvent } = + appletModel.hooks.useUserEvents({ + activityId, + eventId, + }) const { saveItemAnswer, saveItemAdditionalText } = appletModel.hooks.useSaveItemAnswer({ activityId, @@ -161,7 +162,10 @@ export const AssessmentPassingScreen = (props: Props) => { const onItemAdditionalTextChange = (value: string) => { saveItemAdditionalText(item.id, value) - // TODO: saveSetAdditionalTextUserEvent + saveSetAdditionalTextUserEvent({ + ...item, + additionalText: value, + }) } useAutoForward({ From 6c08cef9cf54476cf3a91a11726072c07d7a89d8 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 31 Jan 2024 15:48:15 +0000 Subject: [PATCH 14/19] Fix typo --- src/entities/activity/lib/helpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/activity/lib/helpers.ts b/src/entities/activity/lib/helpers.ts index 9ef4d1381..1a22ccb19 100644 --- a/src/entities/activity/lib/helpers.ts +++ b/src/entities/activity/lib/helpers.ts @@ -15,7 +15,7 @@ export function isSupportedActivity(itemResponseTypes?: Array { return [ @@ -40,7 +40,7 @@ export const supportsAdditonalResponseField = ( * @returns Whether the item has an additional response field */ export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => { - return supportsAdditonalResponseField(item) && item.config.additionalResponseOption.textInputOption + return supportsAdditionalResponseField(item) && item.config.additionalResponseOption.textInputOption } /** @@ -49,5 +49,5 @@ export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => * @returns Whether the item requires an additional response */ export const requiresAdditionalResponse = (item: appletModel.ItemRecord): boolean => { - return supportsAdditonalResponseField(item) && item.config.additionalResponseOption.textInputRequired + return supportsAdditionalResponseField(item) && item.config.additionalResponseOption.textInputRequired } From 4871f22bb4f32f5ce9471fe7ecd5568a6e7366c2 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 31 Jan 2024 16:09:04 +0000 Subject: [PATCH 15/19] Add helper unit tests --- src/entities/activity/lib/helpers.test.ts | 181 ++++++++++++++++++++++ src/entities/activity/lib/helpers.ts | 23 +-- 2 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/entities/activity/lib/helpers.test.ts diff --git a/src/entities/activity/lib/helpers.test.ts b/src/entities/activity/lib/helpers.test.ts new file mode 100644 index 000000000..75560e571 --- /dev/null +++ b/src/entities/activity/lib/helpers.test.ts @@ -0,0 +1,181 @@ +import { hasAdditionalResponse, requiresAdditionalResponse, supportsAdditionalResponseField } from "./helpers" + +describe("Activity helpers", () => { + describe("supportsAdditionalResponseField", () => { + it("Text item should return false", () => { + expect(supportsAdditionalResponseField({ responseType: "text" })).toEqual(false) + }) + + it("Splash screen item should return false", () => { + expect(supportsAdditionalResponseField({ responseType: "splashScreen" })).toEqual(false) + }) + + it("Message item should return false", () => { + expect(supportsAdditionalResponseField({ responseType: "message" })).toEqual(false) + }) + + it("Checkbox item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "multiSelect" })).toEqual(true) + }) + + it("Radio item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "singleSelect" })).toEqual(true) + }) + + it("Slider item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "slider" })).toEqual(true) + }) + + it("Date item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "date" })).toEqual(true) + }) + + it("Number select item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "numberSelect" })).toEqual(true) + }) + + it("Time item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "time" })).toEqual(true) + }) + + it("Time range item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "timeRange" })).toEqual(true) + }) + + it("Audio player item should return true", () => { + expect(supportsAdditionalResponseField({ responseType: "audioPlayer" })).toEqual(true) + }) + }) + + describe("hasAdditionalResponse", () => { + it("Unsupported item should return false", () => { + expect( + hasAdditionalResponse({ + responseType: "text", + config: { + removeBackButton: false, + skippableItem: false, + maxResponseLength: 300, + correctAnswerRequired: false, + correctAnswer: "", + numericalResponseRequired: false, + responseDataIdentifier: false, + responseRequired: false, + }, + }), + ).toEqual(false) + }) + + it("Supported item with additional response option disabled should return false", () => { + expect( + hasAdditionalResponse({ + responseType: "singleSelect", + config: { + removeBackButton: false, + skippableItem: false, + timer: null, + randomizeOptions: false, + addScores: false, + setAlerts: false, + addTooltip: false, + setPalette: false, + autoAdvance: false, + additionalResponseOption: { + textInputOption: false, + textInputRequired: false, + }, + }, + }), + ).toEqual(false) + }) + + it("Supported item with additional response option enabled should return true", () => { + expect( + hasAdditionalResponse({ + responseType: "singleSelect", + config: { + removeBackButton: false, + skippableItem: false, + timer: null, + randomizeOptions: false, + addScores: false, + setAlerts: false, + addTooltip: false, + setPalette: false, + autoAdvance: false, + additionalResponseOption: { + textInputOption: true, + textInputRequired: false, + }, + }, + }), + ).toEqual(true) + }) + }) + + describe("requiresAdditionalResponse", () => { + it("Unsupported item should return false", () => { + expect( + requiresAdditionalResponse({ + responseType: "text", + config: { + removeBackButton: false, + skippableItem: false, + maxResponseLength: 300, + correctAnswerRequired: false, + correctAnswer: "", + numericalResponseRequired: false, + responseDataIdentifier: false, + responseRequired: false, + }, + }), + ).toEqual(false) + }) + + it("Supported item with optional additional response should return false", () => { + expect( + requiresAdditionalResponse({ + responseType: "singleSelect", + config: { + removeBackButton: false, + skippableItem: false, + timer: null, + randomizeOptions: false, + addScores: false, + setAlerts: false, + addTooltip: false, + setPalette: false, + autoAdvance: false, + additionalResponseOption: { + textInputOption: true, + textInputRequired: false, + }, + }, + }), + ).toEqual(false) + }) + + it("Supported item with required additional response should return true", () => { + expect( + requiresAdditionalResponse({ + responseType: "singleSelect", + config: { + removeBackButton: false, + skippableItem: false, + timer: null, + randomizeOptions: false, + addScores: false, + setAlerts: false, + addTooltip: false, + setPalette: false, + autoAdvance: false, + additionalResponseOption: { + textInputOption: true, + textInputRequired: true, + }, + }, + }), + ).toEqual(true) + }) + }) +}) diff --git a/src/entities/activity/lib/helpers.ts b/src/entities/activity/lib/helpers.ts index 1a22ccb19..73d8355f5 100644 --- a/src/entities/activity/lib/helpers.ts +++ b/src/entities/activity/lib/helpers.ts @@ -16,22 +16,11 @@ export function isSupportedActivity(itemResponseTypes?: Array, ): item is appletModel.ItemWithAdditionalResponse => { - return [ - "singleSelect", - "multiSelect", - "slider", - "date", - "numberSelect", - "time", - "timeRange", - "drawing", - "photo", - "video", - "geolocation", - "audio", - ].includes(item.responseType) + return ["singleSelect", "multiSelect", "slider", "date", "numberSelect", "time", "timeRange", "audioPlayer"].includes( + item.responseType, + ) } /** @@ -39,7 +28,7 @@ export const supportsAdditionalResponseField = ( * @param item Any item, even those that don't support additional response fields * @returns Whether the item has an additional response field */ -export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => { +export const hasAdditionalResponse = (item: Pick): boolean => { return supportsAdditionalResponseField(item) && item.config.additionalResponseOption.textInputOption } @@ -48,6 +37,6 @@ export const hasAdditionalResponse = (item: appletModel.ItemRecord): boolean => * @param item Any item, even those that don't support additional response fields * @returns Whether the item requires an additional response */ -export const requiresAdditionalResponse = (item: appletModel.ItemRecord): boolean => { +export const requiresAdditionalResponse = (item: Pick): boolean => { return supportsAdditionalResponseField(item) && item.config.additionalResponseOption.textInputRequired } From dc3ac26f47308b02a1974803b4b3a97c7afa2f6a Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Wed, 31 Jan 2024 16:18:47 +0000 Subject: [PATCH 16/19] Add tests for isSupportedActivity --- src/entities/activity/lib/helpers.test.ts | 63 ++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/entities/activity/lib/helpers.test.ts b/src/entities/activity/lib/helpers.test.ts index 75560e571..db126a61e 100644 --- a/src/entities/activity/lib/helpers.test.ts +++ b/src/entities/activity/lib/helpers.test.ts @@ -1,6 +1,67 @@ -import { hasAdditionalResponse, requiresAdditionalResponse, supportsAdditionalResponseField } from "./helpers" +import { + hasAdditionalResponse, + isSupportedActivity, + requiresAdditionalResponse, + supportsAdditionalResponseField, +} from "./helpers" + +import { ItemResponseTypeDTO } from "~/shared/api" describe("Activity helpers", () => { + describe("isSupportedActivity", () => { + it("Returns false if no response types are provided", () => { + expect(isSupportedActivity()).toEqual(false) + }) + + it("Returns false for unsupported response types", () => { + const unsupportedResponseTypes: ItemResponseTypeDTO[] = [ + "geolocation", + "drawing", + "photo", + "video", + "sliderRows", + "singleSelectRows", + "multiSelectRows", + "audio", + ] + + expect(isSupportedActivity(unsupportedResponseTypes)).toEqual(false) + }) + + it("Returns false for a mix of supported and unsupported response types", () => { + const mixedResponseTypes: ItemResponseTypeDTO[] = [ + "text", + "geolocation", + "drawing", + "photo", + "video", + "sliderRows", + "singleSelectRows", + "multiSelectRows", + "audio", + ] + + expect(isSupportedActivity(mixedResponseTypes)).toEqual(false) + }) + + it("Returns true if all response types are supported", () => { + const supportedResponseTypes: ItemResponseTypeDTO[] = [ + "text", + "singleSelect", + "multiSelect", + "slider", + "numberSelect", + "message", + "date", + "time", + "timeRange", + "audioPlayer", + ] + + expect(isSupportedActivity(supportedResponseTypes)).toEqual(true) + }) + }) + describe("supportsAdditionalResponseField", () => { it("Text item should return false", () => { expect(supportsAdditionalResponseField({ responseType: "text" })).toEqual(false) From 57fee78e2aa2be2b09e49bac3fd0c6620771f248 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Thu, 1 Feb 2024 02:13:37 +0000 Subject: [PATCH 17/19] Simplify AdditionalTextResponse component --- .../activity/ui/AdditionalTextResponse.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/entities/activity/ui/AdditionalTextResponse.tsx b/src/entities/activity/ui/AdditionalTextResponse.tsx index 1069ca0b9..6d3101437 100644 --- a/src/entities/activity/ui/AdditionalTextResponse.tsx +++ b/src/entities/activity/ui/AdditionalTextResponse.tsx @@ -6,21 +6,7 @@ type AdditionalTextResponseProps = { } export const AdditionalTextResponse = ({ value, onValueChange }: AdditionalTextResponseProps) => { - const onHandleValueChange = (value: string) => { - if (value.length === 0) { - return onValueChange("") - } - - return onValueChange(value) - } - return ( - onHandleValueChange(e.target.value)} - disabled={false} - /> + onValueChange(e.target.value)} disabled={false} /> ) } From 647a71d506a8702c7c8bdaf59959e270700b26c3 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Thu, 1 Feb 2024 22:08:31 +0000 Subject: [PATCH 18/19] Replace `SET_ADDITIONAL_TEXT` with `SET_ANSWER` user event --- .../applet/model/hooks/useUserEvents.ts | 86 +++++++++++++++---- src/entities/applet/model/mapper.ts | 2 + src/entities/applet/model/types.ts | 3 +- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/entities/applet/model/hooks/useUserEvents.ts b/src/entities/applet/model/hooks/useUserEvents.ts index 7d75d0b6c..eb8474676 100644 --- a/src/entities/applet/model/hooks/useUserEvents.ts +++ b/src/entities/applet/model/hooks/useUserEvents.ts @@ -54,6 +54,8 @@ export const useUserEvents = (props: Props) => { if (item.responseType === "text" && userEvents.length > 0) { const lastUserEvent = userEvents[userEvents.length - 1] + // We're only interested in updated the previous event if it is a text item + // Otherwise we create a new event with the subsequent answer below if (lastUserEvent.screen === activityItemScreenId && lastUserEvent.type === "SET_ANSWER") { return dispatch( actions.updateUserEventByIndex({ @@ -71,6 +73,8 @@ export const useUserEvents = (props: Props) => { } } + // In all cases besides the text item, we create a new event to reflect + // the updated answer return dispatch( actions.saveUserEvent({ entityId: props.activityId, @@ -99,38 +103,84 @@ export const useUserEvents = (props: Props) => { const activityItemScreenId = getActivityItemScreenId(props.activityId, item.id) if (userEvents.length > 0) { - const lastUserEvent = userEvents[userEvents.length - 1] - - if (lastUserEvent.screen === activityItemScreenId && lastUserEvent.type === "SET_ADDITIONAL_TEXT") { + const previousUserEvent = userEvents[userEvents.length - 1] + + if ( + previousUserEvent.screen === activityItemScreenId && + previousUserEvent.type === "SET_ANSWER" && + typeof previousUserEvent.response !== "string" + ) { + // We want to always update the previous event when it is a SET_ANSWER event for + // the same item. However, the data model currently only supports additional text + // on events for singleSelect and multiSelect items return dispatch( actions.updateUserEventByIndex({ entityId: props.activityId, eventId: props.eventId, userEventIndex: userEvents.length - 1, userEvent: { - type: "SET_ADDITIONAL_TEXT", + type: "SET_ANSWER", screen: activityItemScreenId, time: Date.now(), - response: item.additionalText, + response: { + // The type doesn't allow setting a null value here, but the flow logic + // prevents proceeding without setting the actual answer so using [] + // should be fine + value: previousUserEvent.response?.value ?? [], + text: item.additionalText, + }, }, }), ) } } - return dispatch( - actions.saveUserEvent({ - entityId: props.activityId, - eventId: props.eventId, - itemId: item.id, - userEvent: { - type: "SET_ADDITIONAL_TEXT", - screen: activityItemScreenId, - time: Date.now(), - response: item.additionalText, - }, - }), - ) + // Create a new event in all other cases + if (item.responseType === "singleSelect" || item.responseType === "multiSelect") { + const response = mapItemAnswerToUserEventResponse(item) + + if (typeof response !== "object") { + // This should never happen since text items don't have additional text + // but the TS compiler doesn't know that + return + } + + return dispatch( + actions.saveUserEvent({ + entityId: props.activityId, + eventId: props.eventId, + itemId: item.id, + userEvent: { + type: "SET_ANSWER", + screen: activityItemScreenId, + time: Date.now(), + response: { + value: response.value, + text: item.additionalText, + }, + }, + }), + ) + } else { + // The user event data model for other item types don't yet + // support additional text, so we just save the additional text + return dispatch( + actions.saveUserEvent({ + entityId: props.activityId, + eventId: props.eventId, + itemId: item.id, + userEvent: { + type: "SET_ANSWER", + screen: activityItemScreenId, + time: Date.now(), + response: { + value: [], + text: item.additionalText, + }, + }, + }), + ) + } }, [activityProgress, dispatch, props.activityId, props.eventId], ) diff --git a/src/entities/applet/model/mapper.ts b/src/entities/applet/model/mapper.ts index 70056613a..7dcd55e88 100644 --- a/src/entities/applet/model/mapper.ts +++ b/src/entities/applet/model/mapper.ts @@ -9,12 +9,14 @@ export const mapItemAnswerToUserEventResponse = (item: ItemRecord): UserEventRes if (responseType === "singleSelect") { return { value: [Number(itemAnswer[0])], + text: item.additionalText ?? undefined, } } if (responseType === "multiSelect") { return { value: itemAnswer.map(answer => Number(answer)), + text: item.additionalText ?? undefined, } } diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index f949a7c7e..ccc11cf33 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -13,12 +13,13 @@ import { TimeRangeItem, } from "~/entities/activity/lib" -export type UserEventTypes = "SET_ANSWER" | "SET_ADDITIONAL_TEXT" | "PREV" | "NEXT" | "SKIP" | "DONE" +export type UserEventTypes = "SET_ANSWER" | "PREV" | "NEXT" | "SKIP" | "DONE" export type UserEventResponse = | string | { value: number[] + text?: string } export type UserEvents = { From d277b40c16cf0afc8885893e53a8fbb8eb0e16f0 Mon Sep 17 00:00:00 2001 From: sultanofcardio Date: Thu, 1 Feb 2024 22:37:46 +0000 Subject: [PATCH 19/19] Update `UserEventResponse` to account for more items with additional text --- .../applet/model/hooks/useUserEvents.ts | 78 ++++++------------- src/entities/applet/model/mapper.ts | 5 +- src/entities/applet/model/types.ts | 2 +- 3 files changed, 30 insertions(+), 55 deletions(-) diff --git a/src/entities/applet/model/hooks/useUserEvents.ts b/src/entities/applet/model/hooks/useUserEvents.ts index eb8474676..d60eee0b0 100644 --- a/src/entities/applet/model/hooks/useUserEvents.ts +++ b/src/entities/applet/model/hooks/useUserEvents.ts @@ -98,6 +98,14 @@ export const useUserEvents = (props: Props) => { return } + const response = mapItemAnswerToUserEventResponse(item) + + if (typeof response !== "object") { + // This should never happen since text items don't have additional text + // but the TS compiler doesn't know that + return + } + const userEvents = activityProgress.userEvents const activityItemScreenId = getActivityItemScreenId(props.activityId, item.id) @@ -108,11 +116,10 @@ export const useUserEvents = (props: Props) => { if ( previousUserEvent.screen === activityItemScreenId && previousUserEvent.type === "SET_ANSWER" && - typeof previousUserEvent.response !== "string" + typeof previousUserEvent.response === "object" && + previousUserEvent.response.text !== undefined ) { - // We want to always update the previous event when it is a SET_ANSWER event for - // the same item. However, the data model currently only supports additional text - // on events for singleSelect and multiSelect items + // Update the text of the previous response if it contains text (to prevent incremental events) return dispatch( actions.updateUserEventByIndex({ entityId: props.activityId, @@ -123,10 +130,7 @@ export const useUserEvents = (props: Props) => { screen: activityItemScreenId, time: Date.now(), response: { - // The type doesn't allow setting a null value here, but the flow logic - // prevents proceeding without setting the actual answer so using [] - // should be fine - value: previousUserEvent.response?.value ?? [], + value: previousUserEvent.response.value, text: item.additionalText, }, }, @@ -136,51 +140,19 @@ export const useUserEvents = (props: Props) => { } // Create a new event in all other cases - if (item.responseType === "singleSelect" || item.responseType === "multiSelect") { - const response = mapItemAnswerToUserEventResponse(item) - - if (typeof response !== "object") { - // This should never happen since text items don't have additional text - // but the TS compiler doesn't know that - return - } - - return dispatch( - actions.saveUserEvent({ - entityId: props.activityId, - eventId: props.eventId, - itemId: item.id, - userEvent: { - type: "SET_ANSWER", - screen: activityItemScreenId, - time: Date.now(), - response: { - value: response.value, - text: item.additionalText, - }, - }, - }), - ) - } else { - // The user event data model for other item types don't yet - // support additional text, so we just save the additional text - return dispatch( - actions.saveUserEvent({ - entityId: props.activityId, - eventId: props.eventId, - itemId: item.id, - userEvent: { - type: "SET_ANSWER", - screen: activityItemScreenId, - time: Date.now(), - response: { - value: [], - text: item.additionalText, - }, - }, - }), - ) - } + return dispatch( + actions.saveUserEvent({ + entityId: props.activityId, + eventId: props.eventId, + itemId: item.id, + userEvent: { + type: "SET_ANSWER", + screen: activityItemScreenId, + time: Date.now(), + response, + }, + }), + ) }, [activityProgress, dispatch, props.activityId, props.eventId], ) diff --git a/src/entities/applet/model/mapper.ts b/src/entities/applet/model/mapper.ts index 7dcd55e88..ff4642204 100644 --- a/src/entities/applet/model/mapper.ts +++ b/src/entities/applet/model/mapper.ts @@ -20,7 +20,10 @@ export const mapItemAnswerToUserEventResponse = (item: ItemRecord): UserEventRes } } - return itemAnswer[0] + return { + value: itemAnswer[0], + text: item.additionalText ?? undefined, + } } export function mapItemToRecord(item: ActivityItemDetailsDTO): ItemRecord { diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index ccc11cf33..39d9b3b5e 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -18,7 +18,7 @@ export type UserEventTypes = "SET_ANSWER" | "PREV" | "NEXT" | "SKIP" | "DONE" export type UserEventResponse = | string | { - value: number[] + value: string | string[] | number[] text?: string }