Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[M2-4833] Feature: Additional text response #356

Merged
merged 19 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions src/entities/activity/lib/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
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)
})

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)
})
})
})
32 changes: 32 additions & 0 deletions src/entities/activity/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<ItemResponseTypeDTO>): boolean {
Expand All @@ -8,3 +9,34 @@ export function isSupportedActivity(itemResponseTypes?: Array<ItemResponseTypeDT

return itemResponseTypes.every(type => 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 supportsAdditionalResponseField = (
item: Pick<appletModel.ItemRecord, "responseType">,
): item is appletModel.ItemWithAdditionalResponse => {
return ["singleSelect", "multiSelect", "slider", "date", "numberSelect", "time", "timeRange", "audioPlayer"].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: Pick<appletModel.ItemRecord, "responseType" | "config">): boolean => {
return supportsAdditionalResponseField(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: Pick<appletModel.ItemRecord, "responseType" | "config">): boolean => {
return supportsAdditionalResponseField(item) && item.config.additionalResponseOption.textInputRequired
}
1 change: 1 addition & 0 deletions src/entities/activity/lib/types/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface ActivityItemBase {
config: Config
responseValues: ResponseValues
answer: Answer
additionalText?: string | null
conditionalLogic: ConditionalLogic | null
}

Expand Down
16 changes: 16 additions & 0 deletions src/entities/activity/ui/ActivityCardItem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useMemo } from "react"

import { AdditionalTextResponse } from "./AdditionalTextResponse"
import { ItemPicker } from "./items/ItemPicker"
import { hasAdditionalResponse, requiresAdditionalResponse } 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
Expand All @@ -13,6 +16,8 @@ type ActivityCardItemProps = {

onValueChange: (value: string[]) => void

onItemAdditionalTextChange: (value: string) => void

replaceText: (value: string) => string

step: number
Expand All @@ -27,13 +32,16 @@ export const ActivityCardItem = ({
step,
prevStep,
onValueChange,
onItemAdditionalTextChange,
}: ActivityCardItemProps) => {
const questionText = useMemo(() => {
return replaceText(item.question)
}, [item.question, replaceText])

const isOptionalFlagHidden = ["message", "audioPlayer", "splashScreen"].includes(item.responseType)

const { t } = useCustomTranslation()

return (
<SliderAnimation step={step} prevStep={prevStep ?? step}>
<CardItem
Expand All @@ -42,6 +50,14 @@ export const ActivityCardItem = ({
isOptional={!isOptionalFlagHidden && (item.config.skippableItem || allowToSkipAllItems)}>
<ItemPicker item={item} onValueChange={onValueChange} isDisabled={false} replaceText={replaceText} />
</CardItem>
{hasAdditionalResponse(item) && (
<CardItem
markdown={t("additional.additional_text")}
watermark={watermark}
isOptional={!requiresAdditionalResponse(item)}>
<AdditionalTextResponse value={item.additionalText || ""} onValueChange={onItemAdditionalTextChange} />
</CardItem>
)}
</SliderAnimation>
)
}
12 changes: 12 additions & 0 deletions src/entities/activity/ui/AdditionalTextResponse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import TextField from "@mui/material/TextField"

type AdditionalTextResponseProps = {
value: string
onValueChange: (value: string) => void
}

export const AdditionalTextResponse = ({ value, onValueChange }: AdditionalTextResponseProps) => {
return (
<TextField fullWidth size="small" value={value} onChange={e => onValueChange(e.target.value)} disabled={false} />
)
}
1 change: 1 addition & 0 deletions src/entities/activity/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./ActivityCardItem"
export * from "./ItemCardButtons"
export * from "./AdditionalTextResponse"
15 changes: 15 additions & 0 deletions src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Loading