From 7ebf2ae3685b4d11c30430aa80324ee528dd910b Mon Sep 17 00:00:00 2001 From: Victor Ryabkov <45964820+moiskillnadne@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:46:06 +0800 Subject: [PATCH] [M2-2643] Feature: Matrix: Single Selection per Row Item (#421) * The singleSelectRows type was added into supportableResponseTypes * ActivityItemDetailsDTO was extended with SingleSeletPerRow * Create SingleSelectPerRow type for Redux store * Refactor of Matrix elements to make it reusable * Create the MatrixSingleSelect component * Create mapper to answer * Fix mobile version of matrix grid --------- Co-authored-by: Viktor Riabkov --- src/abstract/lib/constants.ts | 1 + src/entities/activity/lib/types/item.ts | 23 ++++++- src/entities/activity/ui/items/ItemPicker.tsx | 13 +++- .../AxisListItem.tsx | 0 .../MatrixCell.tsx | 2 + .../MatrixHeader.tsx | 0 .../MatrixMultiSelectItem/CheckboxButton.tsx | 40 ++++++++++++ .../MatrixMultiSelectItem/CheckboxGrid.tsx | 48 ++++++++++++++ .../MatrixMultiSelectItem}/index.tsx | 6 +- .../MatrixRow.tsx | 0 .../MatrixSingleSelectItem/RadioButton.tsx | 36 +++++++++++ .../MatrixSingleSelectItem/RadioGrid.tsx | 48 ++++++++++++++ .../Matrix/MatrixSingleSelectItem/index.tsx | 64 +++++++++++++++++++ .../MatrixCheckboxItem/StackedItemsGrid.tsx | 61 ------------------ src/entities/applet/model/types.ts | 8 ++- src/shared/api/types/activity.ts | 4 +- src/shared/api/types/item.ts | 35 +++++++++- .../ui/Items/SelectBase/SelectBaseBox.tsx | 3 +- src/widgets/ActivityDetails/model/mappers.ts | 14 ++++ 19 files changed, 334 insertions(+), 72 deletions(-) rename src/entities/activity/ui/items/{MatrixCheckboxItem => Matrix}/AxisListItem.tsx (100%) rename src/entities/activity/ui/items/{MatrixCheckboxItem => Matrix}/MatrixCell.tsx (93%) rename src/entities/activity/ui/items/{MatrixCheckboxItem => Matrix}/MatrixHeader.tsx (100%) create mode 100644 src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxButton.tsx create mode 100644 src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxGrid.tsx rename src/entities/activity/ui/items/{MatrixCheckboxItem => Matrix/MatrixMultiSelectItem}/index.tsx (94%) rename src/entities/activity/ui/items/{MatrixCheckboxItem => Matrix}/MatrixRow.tsx (100%) create mode 100644 src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/RadioButton.tsx create mode 100644 src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/RadioGrid.tsx create mode 100644 src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/index.tsx delete mode 100644 src/entities/activity/ui/items/MatrixCheckboxItem/StackedItemsGrid.tsx diff --git a/src/abstract/lib/constants.ts b/src/abstract/lib/constants.ts index 75ee932a3..3f94a37cb 100644 --- a/src/abstract/lib/constants.ts +++ b/src/abstract/lib/constants.ts @@ -14,4 +14,5 @@ export const supportableResponseTypes = [ 'timeRange', 'audioPlayer', 'multiSelectRows', + 'singleSelectRows', ]; diff --git a/src/entities/activity/lib/types/item.ts b/src/entities/activity/lib/types/item.ts index 43fd1101b..7a070d771 100644 --- a/src/entities/activity/lib/types/item.ts +++ b/src/entities/activity/lib/types/item.ts @@ -2,8 +2,9 @@ import { ConditionalLogic } from '~/shared/api'; export type DefaultAnswer = Array; export type MatrixMultiSelectAnswer = Array>; +export type SingleMultiSelectAnswer = Array; -export type Answer = DefaultAnswer | MatrixMultiSelectAnswer; +export type Answer = DefaultAnswer | MatrixMultiSelectAnswer | SingleMultiSelectAnswer; export type ActivityItemType = | 'text' @@ -313,3 +314,23 @@ export type MatrixSelectRow = { rowImage: string | null; tooltip: string | null; }; + +export interface SingleSelectionRowsItem extends ActivityItemBase { + responseType: 'singleSelectRows'; + config: SingleSelectionRowsItemConfig; + responseValues: SingleSelectionRowsItemResponseValues; + answer: SingleMultiSelectAnswer; +} + +export type SingleSelectionRowsItemConfig = ButtonsConfig & + TimerConfig & { + addScores: boolean; + setAlerts: boolean; + addTooltip: boolean; + }; + +export type SingleSelectionRowsItemResponseValues = { + rows: Array; + options: Array; + dataMatrix: DataMatrix; +}; diff --git a/src/entities/activity/ui/items/ItemPicker.tsx b/src/entities/activity/ui/items/ItemPicker.tsx index 9a0ef5cbd..5daa1d5c5 100644 --- a/src/entities/activity/ui/items/ItemPicker.tsx +++ b/src/entities/activity/ui/items/ItemPicker.tsx @@ -1,7 +1,8 @@ import { AudioPlayerItem } from './AudioPlayerItem'; import { CheckboxItem } from './CheckboxItem'; import { DateItem } from './DateItem'; -import { MatrixCheckboxItem } from './MatrixCheckboxItem'; +import { MatrixCheckboxItem } from './Matrix/MatrixMultiSelectItem'; +import { MatrixRadioItem } from './Matrix/MatrixSingleSelectItem'; import { RadioItem } from './RadioItem'; import { SelectorItem } from './SelectorItem'; import { SliderItem } from './SliderItem'; @@ -103,6 +104,16 @@ export const ItemPicker = ({ item, onValueChange, isDisabled, replaceText }: Ite /> ); + case 'singleSelectRows': + return ( + + ); + default: return <>; } diff --git a/src/entities/activity/ui/items/MatrixCheckboxItem/AxisListItem.tsx b/src/entities/activity/ui/items/Matrix/AxisListItem.tsx similarity index 100% rename from src/entities/activity/ui/items/MatrixCheckboxItem/AxisListItem.tsx rename to src/entities/activity/ui/items/Matrix/AxisListItem.tsx diff --git a/src/entities/activity/ui/items/MatrixCheckboxItem/MatrixCell.tsx b/src/entities/activity/ui/items/Matrix/MatrixCell.tsx similarity index 93% rename from src/entities/activity/ui/items/MatrixCheckboxItem/MatrixCell.tsx rename to src/entities/activity/ui/items/Matrix/MatrixCell.tsx index 69e8ff645..0e28b475d 100644 --- a/src/entities/activity/ui/items/MatrixCheckboxItem/MatrixCell.tsx +++ b/src/entities/activity/ui/items/Matrix/MatrixCell.tsx @@ -15,6 +15,7 @@ export const MatrixCell = ({ children, isRowLabel }: Props) => { return ( { width={isRowLabel ? rowLabelWidth : '100%'} minWidth={lessThanTarget ? '70px' : '142px'} maxWidth="400px" + data-testid="matrix-cell" > {children} diff --git a/src/entities/activity/ui/items/MatrixCheckboxItem/MatrixHeader.tsx b/src/entities/activity/ui/items/Matrix/MatrixHeader.tsx similarity index 100% rename from src/entities/activity/ui/items/MatrixCheckboxItem/MatrixHeader.tsx rename to src/entities/activity/ui/items/Matrix/MatrixHeader.tsx diff --git a/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxButton.tsx b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxButton.tsx new file mode 100644 index 000000000..8a2898c79 --- /dev/null +++ b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxButton.tsx @@ -0,0 +1,40 @@ +import Box from '@mui/material/Box'; + +import { MatrixCell } from '../MatrixCell'; + +import { CheckboxItem, SelectBaseBox } from '~/shared/ui'; +import { useCustomMediaQuery } from '~/shared/utils'; + +type Props = { + id: string; + isChecked: boolean; + text: string; + + onChange: () => void; +}; + +export const CheckboxButton = ({ id, isChecked, text, onChange }: Props) => { + const { lessThanSM } = useCustomMediaQuery(); + + return ( + + + + + + + + ); +}; diff --git a/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxGrid.tsx b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxGrid.tsx new file mode 100644 index 000000000..eb26b9224 --- /dev/null +++ b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/CheckboxGrid.tsx @@ -0,0 +1,48 @@ +import Box from '@mui/material/Box'; + +import { CheckboxButton } from './CheckboxButton'; +import { MatrixMultiSelectAnswer, MatrixSelectOption, MatrixSelectRow } from '../../../../lib'; +import { MatrixHeader } from '../MatrixHeader'; +import { MatrixRow } from '../MatrixRow'; + +type Props = { + options: Array; + rows: Array; + + onChange: (rowIndex: number, optionIndex: number, value: string) => void; + values: MatrixMultiSelectAnswer; +}; + +export const CheckboxGrid = ({ rows, options, onChange, values }: Props) => { + return ( + + + + {rows.map((row, rowI) => { + const isEven = rowI % 2 === 0; + + return ( + + {options.map((option, optionI) => { + const isChecked = Boolean(values[rowI]?.[optionI]); + + return ( + onChange(rowI, optionI, option.text)} + /> + ); + })} + + ); + })} + + ); +}; diff --git a/src/entities/activity/ui/items/MatrixCheckboxItem/index.tsx b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/index.tsx similarity index 94% rename from src/entities/activity/ui/items/MatrixCheckboxItem/index.tsx rename to src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/index.tsx index e338f67ab..87fd89202 100644 --- a/src/entities/activity/ui/items/MatrixCheckboxItem/index.tsx +++ b/src/entities/activity/ui/items/Matrix/MatrixMultiSelectItem/index.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import { StackedItemsGrid } from './StackedItemsGrid'; -import { MatrixMultiSelectAnswer, MultiSelectionRowsItem } from '../../../lib'; +import { CheckboxGrid } from './CheckboxGrid'; +import { MatrixMultiSelectAnswer, MultiSelectionRowsItem } from '../../../../lib'; type Props = { item: MultiSelectionRowsItem; @@ -57,7 +57,7 @@ export const MatrixCheckboxItem = ({ item, values, onValueChange, replaceText }: }; return ( - void; +}; + +export const RadioButton = ({ id, text, isChecked, onChange }: Props) => { + return ( + + + + + + + + ); +}; diff --git a/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/RadioGrid.tsx b/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/RadioGrid.tsx new file mode 100644 index 000000000..de1679e6c --- /dev/null +++ b/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/RadioGrid.tsx @@ -0,0 +1,48 @@ +import Box from '@mui/material/Box'; + +import { RadioButton } from './RadioButton'; +import { MatrixSelectOption, MatrixSelectRow, SingleMultiSelectAnswer } from '../../../../lib'; +import { MatrixHeader } from '../MatrixHeader'; +import { MatrixRow } from '../MatrixRow'; + +type Props = { + options: Array; + rows: Array; + + onChange: (rowIndex: number, value: string) => void; + values: SingleMultiSelectAnswer; +}; + +export const RadioGrid = ({ rows, options, onChange, values }: Props) => { + return ( + + + + {rows.map((row, rowI) => { + const isEven = rowI % 2 === 0; + + return ( + + {options.map((option) => { + const isChecked = option.text === values[rowI]; + + return ( + onChange(rowI, option.text)} + /> + ); + })} + + ); + })} + + ); +}; diff --git a/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/index.tsx b/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/index.tsx new file mode 100644 index 000000000..8c73bf8eb --- /dev/null +++ b/src/entities/activity/ui/items/Matrix/MatrixSingleSelectItem/index.tsx @@ -0,0 +1,64 @@ +import { useMemo } from 'react'; + +import { RadioGrid } from './RadioGrid'; +import { SingleMultiSelectAnswer, SingleSelectionRowsItem } from '../../../../lib'; + +type Props = { + item: SingleSelectionRowsItem; + values: SingleMultiSelectAnswer; + + onValueChange: (value: SingleMultiSelectAnswer) => void; + replaceText: (value: string) => string; +}; + +export const MatrixRadioItem = ({ item, values, onValueChange, replaceText }: Props) => { + const { options, rows } = item.responseValues; + + const memoizedOptions = useMemo(() => { + return options.map((el) => ({ + ...el, + tooltip: el.tooltip ? replaceText(el.tooltip) : null, + })); + }, [options, replaceText]); + + const memoizedRows = useMemo(() => { + return rows.map((el) => ({ + ...el, + tooltip: el.tooltip ? replaceText(el.tooltip) : null, + })); + }, [replaceText, rows]); + + const memoizedValues = useMemo(() => { + const initialAnswer = memoizedRows.map(() => null); + + return values.length ? values : initialAnswer; + }, [memoizedRows, values]); + + const handleValueChange = (rowIndex: number, value: string) => { + const newValues = memoizedValues.map((row, i) => { + if (i === rowIndex) { + const hasAnswer = row !== null; + const isSameAnswer = row === value; + + if (hasAnswer && isSameAnswer) { + return null; + } else { + return value; + } + } + + return row; + }); + + onValueChange(newValues); + }; + + return ( + + ); +}; diff --git a/src/entities/activity/ui/items/MatrixCheckboxItem/StackedItemsGrid.tsx b/src/entities/activity/ui/items/MatrixCheckboxItem/StackedItemsGrid.tsx deleted file mode 100644 index 6547e5e2e..000000000 --- a/src/entities/activity/ui/items/MatrixCheckboxItem/StackedItemsGrid.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import Box from '@mui/material/Box'; - -import { MatrixCell } from './MatrixCell'; -import { MatrixHeader } from './MatrixHeader'; -import { MatrixRow } from './MatrixRow'; -import { MatrixMultiSelectAnswer, MatrixSelectOption, MatrixSelectRow } from '../../../lib'; - -import { CheckboxItem, SelectBaseBox } from '~/shared/ui'; - -type Props = { - options: Array; - rows: Array; - - onChange: (rowIndex: number, optionIndex: number, value: string) => void; - values: MatrixMultiSelectAnswer; -}; - -export const StackedItemsGrid = ({ rows, options, onChange, values }: Props) => { - return ( - - - - {rows.map((row, rowI) => { - const isEven = rowI % 2 === 0; - - return ( - - {options.map((option, optionI) => { - const isChecked = Boolean(values[rowI]?.[optionI]); - - return ( - - - onChange(rowI, optionI, option.text)} - checked={isChecked} - justifyContent="center" - > - - - - - ); - })} - - ); - })} - - ); -}; diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index e4a0fed97..b3a761354 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -9,6 +9,8 @@ import { MultiSelectionRowsItem, RadioItem, SelectorItem, + SingleMultiSelectAnswer, + SingleSelectionRowsItem, SliderItem, SplashScreenItem, TextItem, @@ -42,7 +44,8 @@ export type UserEventResponse = | DayMonthYearDTO | HourMinuteDTO | TimeRangeUserEventDto - | MatrixMultiSelectAnswer; + | MatrixMultiSelectAnswer + | SingleMultiSelectAnswer; text?: string; }; @@ -65,7 +68,8 @@ export type ItemRecord = | TimeItem | TimeRangeItem | AudioPlayerItem - | MultiSelectionRowsItem; + | MultiSelectionRowsItem + | SingleSelectionRowsItem; export type ItemWithAdditionalResponse = Extract< ItemRecord, diff --git a/src/shared/api/types/activity.ts b/src/shared/api/types/activity.ts index f601e6fea..fd64c4cb5 100644 --- a/src/shared/api/types/activity.ts +++ b/src/shared/api/types/activity.ts @@ -7,6 +7,7 @@ import { MultiSelectionRowsItemDTO, RadioItemDTO, SelectorItemDTO, + SingleSelectionRowsItemDTO, SliderItemDTO, TextItemDTO, TimeItemDTO, @@ -58,7 +59,8 @@ export type ActivityItemDetailsDTO = | TimeItemDTO | TimeRangeItemDTO | AudioPlayerItemDTO - | MultiSelectionRowsItemDTO; + | MultiSelectionRowsItemDTO + | SingleSelectionRowsItemDTO; export type AnswerPayload = { appletId: ID; diff --git a/src/shared/api/types/item.ts b/src/shared/api/types/item.ts index 20a46d59a..4d4e5dc70 100644 --- a/src/shared/api/types/item.ts +++ b/src/shared/api/types/item.ts @@ -43,7 +43,8 @@ export type ConfigDTO = | TimeItemConfigDTO | TimeRangeItemConfigDTO | AudioPlayerItemConfigDTO - | MultiSelectionRowsItemConfigDTO; + | MultiSelectionRowsItemConfigDTO + | SingleSelectionRowsItemConfigDTO; export type ResponseValuesDTO = | EmptyResponseValuesDTO @@ -52,7 +53,8 @@ export type ResponseValuesDTO = | SliderItemResponseValuesDTO | SelectorItemResponseValues | AudioPlayerItemResponseValuesDTO - | MultiSelectionRowsItemResponseValuesDTO; + | MultiSelectionRowsItemResponseValuesDTO + | SingleSelectionRowsItemResponseValuesDTO; type EmptyResponseValuesDTO = null; @@ -302,3 +304,32 @@ type MultiSelectionRowsItemResponseValuesDTO = { }>; dataMatrix: DataMatrixDto; }; + +export interface SingleSelectionRowsItemDTO extends ItemDetailsBaseDTO { + responseType: 'multiSelectRows'; + config: SingleSelectionRowsItemConfigDTO; + responseValues: SingleSelectionRowsItemResponseValuesDTO; +} + +type SingleSelectionRowsItemConfigDTO = ButtonsConfigDTO & + TimerConfigDTO & { + addScores: boolean; + setAlerts: boolean; + addTooltip: boolean; + }; + +type SingleSelectionRowsItemResponseValuesDTO = { + rows: Array<{ + id: string; + rowName: string; + rowImage: string | null; + tooltip: string | null; + }>; + options: Array<{ + id: string; + text: string; + image: string | null; + tooltip: string | null; + }>; + dataMatrix: DataMatrixDto; +}; diff --git a/src/shared/ui/Items/SelectBase/SelectBaseBox.tsx b/src/shared/ui/Items/SelectBase/SelectBaseBox.tsx index 27d9e4966..b893a0382 100644 --- a/src/shared/ui/Items/SelectBase/SelectBaseBox.tsx +++ b/src/shared/ui/Items/SelectBase/SelectBaseBox.tsx @@ -5,6 +5,7 @@ import Box from '@mui/material/Box'; import { Theme } from '~/shared/constants'; type Props = PropsWithChildren<{ + padding?: string; justifyContent?: | 'flex-start' | 'center' @@ -40,7 +41,7 @@ export const SelectBaseBox = (props: Props) => { gap="12px" className="response-option" borderRadius="12px" - padding="16px" + padding={props.padding ? props.padding : '16px'} border={`2px solid ${borderColor}`} bgcolor={props.color ? props.color : backgroundColor} onClick={props.onHandleChange} diff --git a/src/widgets/ActivityDetails/model/mappers.ts b/src/widgets/ActivityDetails/model/mappers.ts index bee79abd6..e53a24ead 100644 --- a/src/widgets/ActivityDetails/model/mappers.ts +++ b/src/widgets/ActivityDetails/model/mappers.ts @@ -8,6 +8,7 @@ import { MultiSelectionRowsItem, RadioItem, SelectorItem, + SingleSelectionRowsItem, SliderItem, TextItem, TimeItem, @@ -69,6 +70,9 @@ export function mapToAnswers( case 'multiSelectRows': return convertToMatrixMultiSelectAnswer(item); + case 'singleSelectRows': + return convertToMatrixSingleSelectAnswer(item); + default: return null; } @@ -252,6 +256,16 @@ function convertToMatrixMultiSelectAnswer( }; } +function convertToMatrixSingleSelectAnswer(item: SingleSelectionRowsItem) { + return { + answer: { + value: item.answer, + text: item.additionalText || null, + }, + itemId: item.id, + }; +} + export function mapAlerts(items: Array): Array { const alerts = items.map((item) => { switch (item.responseType) {